Merge branch 'master' into jnater/courseware_tests
This commit is contained in:
@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Blades: Small UX fix on capa multiple-choice problems. Make labels only
|
||||
as wide as the text to reduce accidental choice selections.
|
||||
|
||||
Studio: Remove XML from the video component editor. All settings are
|
||||
moved to be edited as metadata.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def i_am_on_advanced_course_settings(step):
|
||||
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
css = 'a.%s-button' % name.lower()
|
||||
css = 'a.action-%s' % name.lower()
|
||||
|
||||
# Save was clicked if either the save notification bar is gone, or we have a error notification
|
||||
# overlaying it (expected in the case of typing Object into display_name).
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
@@ -13,8 +12,13 @@ import time
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
_COURSE_NAME = 'Robot Super Course'
|
||||
_COURSE_NUM = '999'
|
||||
_COURSE_ORG = 'MITx'
|
||||
|
||||
########### 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
|
||||
@@ -54,6 +58,7 @@ def i_have_opened_a_new_course(_step):
|
||||
####### HELPER FUNCTIONS ##############
|
||||
def open_new_course():
|
||||
world.clear_courses()
|
||||
create_studio_user()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
@@ -73,10 +78,11 @@ def create_studio_user(
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
|
||||
def fill_in_course_info(
|
||||
name='Robot Super Course',
|
||||
org='MITx',
|
||||
num='101'):
|
||||
name=_COURSE_NAME,
|
||||
org=_COURSE_ORG,
|
||||
num=_COURSE_NUM):
|
||||
world.css_fill('.new-course-name', name)
|
||||
world.css_fill('.new-course-org', org)
|
||||
world.css_fill('.new-course-number', num)
|
||||
@@ -85,10 +91,7 @@ def fill_in_course_info(
|
||||
def log_into_studio(
|
||||
uname='robot',
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
|
||||
create_studio_user(uname=uname, email=email, is_staff=is_staff)
|
||||
password='test'):
|
||||
|
||||
world.browser.cookies.delete()
|
||||
world.visit('/')
|
||||
@@ -106,14 +109,14 @@ def log_into_studio(
|
||||
|
||||
|
||||
def create_a_course():
|
||||
world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
world.CourseFactory.create(org=_COURSE_ORG, course=_COURSE_NUM, display_name=_COURSE_NAME)
|
||||
|
||||
# Add the user to the instructor group of the course
|
||||
# so they will have the permissions to see it in studio
|
||||
g = world.GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
u = get_user_by_email('robot+studio@edx.org')
|
||||
u.groups.add(g)
|
||||
u.save()
|
||||
course = world.GroupFactory.create(name='instructor_MITx/{course_num}/{course_name}'.format(course_num=_COURSE_NUM, course_name=_COURSE_NAME.replace(" ", "_")))
|
||||
user = get_user_by_email('robot+studio@edx.org')
|
||||
user.groups.add(course)
|
||||
user.save()
|
||||
world.browser.reload()
|
||||
|
||||
course_link_css = 'span.class-name'
|
||||
|
||||
34
cms/djangoapps/contentstore/features/course-team.feature
Normal file
34
cms/djangoapps/contentstore/features/course-team.feature
Normal file
@@ -0,0 +1,34 @@
|
||||
Feature: Course Team
|
||||
As a course author, I want to be able to add others to my team
|
||||
|
||||
Scenario: Users can add other users
|
||||
Given I have opened a new course in Studio
|
||||
And the user "alice" exists
|
||||
And I am viewing the course team settings
|
||||
When I add "alice" to the course team
|
||||
And "alice" logs in
|
||||
Then she does see the course on her page
|
||||
|
||||
Scenario: Added users cannot delete or add other users
|
||||
Given I have opened a new course in Studio
|
||||
And the user "bob" exists
|
||||
And I am viewing the course team settings
|
||||
When I add "bob" to the course team
|
||||
And "bob" logs in
|
||||
Then he cannot delete users
|
||||
And he cannot add users
|
||||
|
||||
Scenario: Users can delete other users
|
||||
Given I have opened a new course in Studio
|
||||
And the user "carol" exists
|
||||
And I am viewing the course team settings
|
||||
When I add "carol" to the course team
|
||||
And I delete "carol" from the course team
|
||||
And "carol" logs in
|
||||
Then she does not see the course on her page
|
||||
|
||||
Scenario: Users cannot add users that do not exist
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the course team settings
|
||||
When I add "dennis" to the course team
|
||||
Then I should see "Could not find user by email address" somewhere on the page
|
||||
67
cms/djangoapps/contentstore/features/course-team.py
Normal file
67
cms/djangoapps/contentstore/features/course-team.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import create_studio_user, log_into_studio, _COURSE_NAME
|
||||
|
||||
PASSWORD = 'test'
|
||||
EMAIL_EXTENSION = '@edx.org'
|
||||
|
||||
|
||||
@step(u'I am viewing the course team settings')
|
||||
def view_grading_settings(_step):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-team a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step(u'the user "([^"]*)" exists$')
|
||||
def create_other_user(_step, name):
|
||||
create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION))
|
||||
|
||||
|
||||
@step(u'I add "([^"]*)" to the course team')
|
||||
def add_other_user(_step, name):
|
||||
new_user_css = 'a.new-user-button'
|
||||
world.css_click(new_user_css)
|
||||
|
||||
email_css = 'input.email-input'
|
||||
f = world.css_find(email_css)
|
||||
f._element.send_keys(name, EMAIL_EXTENSION)
|
||||
|
||||
confirm_css = '#add_user'
|
||||
world.css_click(confirm_css)
|
||||
|
||||
|
||||
@step(u'I delete "([^"]*)" from the course team')
|
||||
def delete_other_user(_step, name):
|
||||
to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION)
|
||||
world.css_click(to_delete_css)
|
||||
|
||||
|
||||
@step(u'"([^"]*)" logs in$')
|
||||
def other_user_login(_step, name):
|
||||
log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION)
|
||||
|
||||
|
||||
@step(u's?he does( not)? see the course on (his|her) page')
|
||||
def see_course(_step, doesnt_see_course, gender):
|
||||
class_css = 'span.class-name'
|
||||
all_courses = world.css_find(class_css)
|
||||
all_names = [item.html for item in all_courses]
|
||||
if doesnt_see_course:
|
||||
assert not _COURSE_NAME in all_names
|
||||
else:
|
||||
assert _COURSE_NAME in all_names
|
||||
|
||||
|
||||
@step(u's?he cannot delete users')
|
||||
def cannot_delete(_step):
|
||||
to_delete_css = 'a.remove-user'
|
||||
assert world.is_css_not_present(to_delete_css)
|
||||
|
||||
|
||||
@step(u's?he cannot add users')
|
||||
def cannot_add(_step):
|
||||
add_css = 'a.new-user'
|
||||
assert world.is_css_not_present(add_css)
|
||||
37
cms/djangoapps/contentstore/features/course-updates.feature
Normal file
37
cms/djangoapps/contentstore/features/course-updates.feature
Normal file
@@ -0,0 +1,37 @@
|
||||
Feature: Course updates
|
||||
As a course author, I want to be able to provide updates to my students
|
||||
|
||||
Scenario: Users can add updates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
When I add a new update with the text "Hello"
|
||||
Then I should see the update "Hello"
|
||||
|
||||
Scenario: Users can edit updates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
When I add a new update with the text "Hello"
|
||||
And I modify the text to "Goodbye"
|
||||
Then I should see the update "Goodbye"
|
||||
|
||||
Scenario: Users can delete updates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
And I add a new update with the text "Hello"
|
||||
When I will confirm all alerts
|
||||
And I delete the update
|
||||
Then I should not see the update "Hello"
|
||||
|
||||
|
||||
Scenario: Users can edit update dates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
And I add a new update with the text "Hello"
|
||||
When I edit the date to "June 1, 2013"
|
||||
Then I should see the date "June 1, 2013"
|
||||
|
||||
Scenario: Users can change handouts
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
When I modify the handout to "<ol>Test</ol>"
|
||||
Then I see the handout "Test"
|
||||
84
cms/djangoapps/contentstore/features/course-updates.py
Normal file
84
cms/djangoapps/contentstore/features/course-updates.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from common import type_in_codemirror
|
||||
|
||||
|
||||
@step(u'I go to the course updates page')
|
||||
def go_to_updates(_step):
|
||||
menu_css = 'li.nav-course-courseware'
|
||||
updates_css = 'li.nav-course-courseware-updates'
|
||||
world.css_click(menu_css)
|
||||
world.css_click(updates_css)
|
||||
|
||||
|
||||
@step(u'I add a new update with the text "([^"]*)"$')
|
||||
def add_update(_step, text):
|
||||
update_css = 'a.new-update-button'
|
||||
world.css_click(update_css)
|
||||
change_text(text)
|
||||
|
||||
|
||||
@step(u'I should( not)? see the update "([^"]*)"$')
|
||||
def check_update(_step, doesnt_see_update, text):
|
||||
update_css = 'div.update-contents'
|
||||
update = world.css_find(update_css)
|
||||
if doesnt_see_update:
|
||||
assert len(update) == 0 or not text in update.html
|
||||
else:
|
||||
assert text in update.html
|
||||
|
||||
|
||||
@step(u'I modify the text to "([^"]*)"$')
|
||||
def modify_update(_step, text):
|
||||
button_css = 'div.post-preview a.edit-button'
|
||||
world.css_click(button_css)
|
||||
change_text(text)
|
||||
|
||||
|
||||
@step(u'I delete the update$')
|
||||
def click_button(_step):
|
||||
button_css = 'div.post-preview a.delete-button'
|
||||
world.css_click(button_css)
|
||||
|
||||
|
||||
@step(u'I edit the date to "([^"]*)"$')
|
||||
def change_date(_step, new_date):
|
||||
button_css = 'div.post-preview a.edit-button'
|
||||
world.css_click(button_css)
|
||||
date_css = 'input.date'
|
||||
date = world.css_find(date_css)
|
||||
for i in range(len(date.value)):
|
||||
date._element.send_keys(Keys.END, Keys.BACK_SPACE)
|
||||
date._element.send_keys(new_date)
|
||||
save_css = 'a.save-button'
|
||||
world.css_click(save_css)
|
||||
|
||||
|
||||
@step(u'I should see the date "([^"]*)"$')
|
||||
def check_date(_step, date):
|
||||
date_css = 'span.date-display'
|
||||
date_html = world.css_find(date_css)
|
||||
assert date == date_html.html
|
||||
|
||||
|
||||
@step(u'I modify the handout to "([^"]*)"$')
|
||||
def edit_handouts(_step, text):
|
||||
edit_css = 'div.course-handouts > a.edit-button'
|
||||
world.css_click(edit_css)
|
||||
change_text(text)
|
||||
|
||||
|
||||
@step(u'I see the handout "([^"]*)"$')
|
||||
def check_handout(_step, handout):
|
||||
handout_css = 'div.handouts-content'
|
||||
handouts = world.css_find(handout_css)
|
||||
assert handout in handouts.html
|
||||
|
||||
|
||||
def change_text(text):
|
||||
type_in_codemirror(0, text)
|
||||
save_css = 'a.save-button'
|
||||
world.css_click(save_css)
|
||||
@@ -10,6 +10,7 @@ from common import *
|
||||
@step('There are no courses$')
|
||||
def no_courses(step):
|
||||
world.clear_courses()
|
||||
create_studio_user()
|
||||
|
||||
|
||||
@step('I click the New Course button$')
|
||||
|
||||
24
cms/djangoapps/contentstore/features/static-pages.feature
Normal file
24
cms/djangoapps/contentstore/features/static-pages.feature
Normal file
@@ -0,0 +1,24 @@
|
||||
Feature: Static Pages
|
||||
As a course author, I want to be able to add static pages
|
||||
|
||||
Scenario: Users can add static pages
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the static pages page
|
||||
When I add a new page
|
||||
Then I should see a "Empty" static page
|
||||
|
||||
Scenario: Users can delete static pages
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the static pages page
|
||||
And I add a new page
|
||||
When I will confirm all alerts
|
||||
And I "delete" the "Empty" page
|
||||
Then I should not see a "Empty" static page
|
||||
|
||||
Scenario: Users can edit static pages
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the static pages page
|
||||
And I add a new page
|
||||
When I "edit" the "Empty" page
|
||||
And I change the name to "New"
|
||||
Then I should see a "New" static page
|
||||
59
cms/djangoapps/contentstore/features/static-pages.py
Normal file
59
cms/djangoapps/contentstore/features/static-pages.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
|
||||
@step(u'I go to the static pages page')
|
||||
def go_to_static(_step):
|
||||
menu_css = 'li.nav-course-courseware'
|
||||
static_css = 'li.nav-course-courseware-pages'
|
||||
world.css_find(menu_css).click()
|
||||
world.css_find(static_css).click()
|
||||
|
||||
|
||||
@step(u'I add a new page')
|
||||
def add_page(_step):
|
||||
button_css = 'a.new-button'
|
||||
world.css_find(button_css).click()
|
||||
|
||||
|
||||
@step(u'I should( not)? see a "([^"]*)" static page$')
|
||||
def see_page(_step, doesnt, page):
|
||||
index = get_index(page)
|
||||
if doesnt:
|
||||
assert index == -1
|
||||
else:
|
||||
assert index != -1
|
||||
|
||||
|
||||
@step(u'I "([^"]*)" the "([^"]*)" page$')
|
||||
def click_edit_delete(_step, edit_delete, page):
|
||||
button_css = 'a.%s-button' % edit_delete
|
||||
index = get_index(page)
|
||||
assert index != -1
|
||||
world.css_find(button_css)[index].click()
|
||||
|
||||
|
||||
@step(u'I change the name to "([^"]*)"$')
|
||||
def change_name(_step, new_name):
|
||||
settings_css = '#settings-mode'
|
||||
world.css_find(settings_css).click()
|
||||
input_css = 'input.setting-input'
|
||||
name_input = world.css_find(input_css)
|
||||
old_name = name_input.value
|
||||
for count in range(len(old_name)):
|
||||
name_input._element.send_keys(Keys.END, Keys.BACK_SPACE)
|
||||
name_input._element.send_keys(new_name)
|
||||
save_button = 'a.save-button'
|
||||
world.css_find(save_button).click()
|
||||
|
||||
|
||||
def get_index(name):
|
||||
page_name_css = 'section[data-type="HTMLModule"]'
|
||||
all_pages = world.css_find(page_name_css)
|
||||
for i in range(len(all_pages)):
|
||||
if all_pages[i].html == '\n {name}\n'.format(name=name):
|
||||
return i
|
||||
return -1
|
||||
@@ -50,7 +50,8 @@ def have_a_course_with_two_sections(step):
|
||||
|
||||
@step(u'I navigate to the course overview page$')
|
||||
def navigate_to_the_course_overview_page(step):
|
||||
log_into_studio(is_staff=True)
|
||||
create_studio_user(is_staff=True)
|
||||
log_into_studio()
|
||||
course_locator = '.class-name'
|
||||
world.css_click(course_locator)
|
||||
|
||||
|
||||
38
cms/djangoapps/contentstore/features/upload.feature
Normal file
38
cms/djangoapps/contentstore/features/upload.feature
Normal file
@@ -0,0 +1,38 @@
|
||||
Feature: Upload Files
|
||||
As a course author, I want to be able to upload files for my students
|
||||
|
||||
Scenario: Users can upload files
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
Then I should see the file "test" was uploaded
|
||||
And The url for the file "test" is valid
|
||||
|
||||
Scenario: Users can update files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
And I upload the file "test"
|
||||
Then I should see only one "test"
|
||||
|
||||
Scenario: Users can delete uploaded files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
And I delete the file "test"
|
||||
Then I should not see the file "test" was uploaded
|
||||
|
||||
Scenario: Users can download files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
Then I can download the correct "test" file
|
||||
|
||||
Scenario: Users can download updated files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
And I modify "test"
|
||||
And I reload the page
|
||||
And I upload the file "test"
|
||||
Then I can download the correct "test" file
|
||||
108
cms/djangoapps/contentstore/features/upload.py
Normal file
108
cms/djangoapps/contentstore/features/upload.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from django.conf import settings
|
||||
import requests
|
||||
import string
|
||||
import random
|
||||
import os
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
HTTP_PREFIX = "http://localhost:8001"
|
||||
|
||||
|
||||
@step(u'I go to the files and uploads page')
|
||||
def go_to_uploads(_step):
|
||||
menu_css = 'li.nav-course-courseware'
|
||||
uploads_css = 'li.nav-course-courseware-uploads'
|
||||
world.css_find(menu_css).click()
|
||||
world.css_find(uploads_css).click()
|
||||
|
||||
|
||||
@step(u'I upload the file "([^"]*)"$')
|
||||
def upload_file(_step, file_name):
|
||||
upload_css = 'a.upload-button'
|
||||
world.css_find(upload_css).click()
|
||||
|
||||
file_css = 'input.file-input'
|
||||
upload = world.css_find(file_css)
|
||||
#uploading the file itself
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
|
||||
upload._element.send_keys(os.path.abspath(path))
|
||||
|
||||
close_css = 'a.close-button'
|
||||
world.css_find(close_css).click()
|
||||
|
||||
|
||||
@step(u'I should( not)? see the file "([^"]*)" was uploaded$')
|
||||
def check_upload(_step, do_not_see_file, file_name):
|
||||
index = get_index(file_name)
|
||||
if do_not_see_file:
|
||||
assert index == -1
|
||||
else:
|
||||
assert index != -1
|
||||
|
||||
|
||||
@step(u'The url for the file "([^"]*)" is valid$')
|
||||
def check_url(_step, file_name):
|
||||
r = get_file(file_name)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
@step(u'I delete the file "([^"]*)"$')
|
||||
def delete_file(_step, file_name):
|
||||
index = get_index(file_name)
|
||||
assert index != -1
|
||||
delete_css = "a.remove-asset-button"
|
||||
world.css_click(delete_css, index=index)
|
||||
|
||||
prompt_confirm_css = 'li.nav-item > a.action-primary'
|
||||
world.css_click(prompt_confirm_css)
|
||||
|
||||
|
||||
@step(u'I should see only one "([^"]*)"$')
|
||||
def no_duplicate(_step, file_name):
|
||||
names_css = 'td.name-col > a.filename'
|
||||
all_names = world.css_find(names_css)
|
||||
only_one = False
|
||||
for i in range(len(all_names)):
|
||||
if file_name == all_names[i].html:
|
||||
only_one = not only_one
|
||||
assert only_one
|
||||
|
||||
|
||||
@step(u'I can download the correct "([^"]*)" file$')
|
||||
def check_download(_step, file_name):
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
|
||||
with open(os.path.abspath(path), 'r') as cur_file:
|
||||
cur_text = cur_file.read()
|
||||
r = get_file(file_name)
|
||||
downloaded_text = r.text
|
||||
assert cur_text == downloaded_text
|
||||
|
||||
|
||||
@step(u'I modify "([^"]*)"$')
|
||||
def modify_upload(_step, file_name):
|
||||
new_text = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10))
|
||||
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
|
||||
with open(os.path.abspath(path), 'w') as cur_file:
|
||||
cur_file.write(new_text)
|
||||
|
||||
|
||||
def get_index(file_name):
|
||||
names_css = 'td.name-col > a.filename'
|
||||
all_names = world.css_find(names_css)
|
||||
for i in range(len(all_names)):
|
||||
if file_name == all_names[i].html:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def get_file(file_name):
|
||||
index = get_index(file_name)
|
||||
assert index != -1
|
||||
|
||||
url_css = 'input.embeddable-xml-input'
|
||||
url = world.css_find(url_css)[index].value
|
||||
return requests.get(HTTP_PREFIX + url)
|
||||
@@ -181,6 +181,6 @@ if SEGMENT_IO_KEY:
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
from .private import *
|
||||
from .private import * # pylint: disable=F0401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -100,11 +100,10 @@ describe "CMS.Views.SystemFeedback click events", ->
|
||||
text: "Save",
|
||||
class: "save-button",
|
||||
click: @primaryClickSpy
|
||||
secondary: [{
|
||||
secondary:
|
||||
text: "Revert",
|
||||
class: "cancel-button",
|
||||
click: @secondaryClickSpy
|
||||
}]
|
||||
)
|
||||
@view.show()
|
||||
|
||||
@@ -124,6 +123,46 @@ describe "CMS.Views.SystemFeedback click events", ->
|
||||
it "should apply class to secondary action", ->
|
||||
expect(@view.$(".action-secondary")).toHaveClass("cancel-button")
|
||||
|
||||
|
||||
describe "CMS.Views.SystemFeedback multiple secondary actions", ->
|
||||
beforeEach ->
|
||||
@secondarySpyOne = jasmine.createSpy('secondarySpyOne')
|
||||
@secondarySpyTwo = jasmine.createSpy('secondarySpyTwo')
|
||||
@view = new CMS.Views.Notification.Warning(
|
||||
title: "No Primary",
|
||||
message: "Pick a secondary action",
|
||||
actions:
|
||||
secondary: [
|
||||
{
|
||||
text: "Option One"
|
||||
class: "option-one"
|
||||
click: @secondarySpyOne
|
||||
}, {
|
||||
text: "Option Two"
|
||||
class: "option-two"
|
||||
click: @secondarySpyTwo
|
||||
}
|
||||
]
|
||||
)
|
||||
@view.show()
|
||||
|
||||
it "should render both", ->
|
||||
expect(@view.el).toContain(".action-secondary.option-one")
|
||||
expect(@view.el).toContain(".action-secondary.option-two")
|
||||
expect(@view.el).not.toContain(".action-secondary.option-one.option-two")
|
||||
expect(@view.$(".action-secondary.option-one")).toContainText("Option One")
|
||||
expect(@view.$(".action-secondary.option-two")).toContainText("Option Two")
|
||||
|
||||
it "should differentiate clicks (1)", ->
|
||||
@view.$(".option-one").click()
|
||||
expect(@secondarySpyOne).toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).not.toHaveBeenCalled()
|
||||
|
||||
it "should differentiate clicks (2)", ->
|
||||
@view.$(".option-two").click()
|
||||
expect(@secondarySpyOne).not.toHaveBeenCalled()
|
||||
expect(@secondarySpyTwo).toHaveBeenCalled()
|
||||
|
||||
describe "CMS.Views.Notification minShown and maxShown", ->
|
||||
beforeEach ->
|
||||
@showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show')
|
||||
|
||||
@@ -25,7 +25,6 @@ $(document).ready(function() {
|
||||
$newComponentTemplatePickers = $('.new-component-templates');
|
||||
$newComponentButton = $('.new-component-button');
|
||||
$spinner = $('<span class="spinner-in-field-icon"></span>');
|
||||
$body.bind('keyup', onKeyUp);
|
||||
|
||||
$('.expand-collapse-icon').bind('click', toggleSubmodules);
|
||||
$('.visibility-options').bind('change', setVisibility);
|
||||
@@ -413,12 +412,6 @@ function hideModal(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e) {
|
||||
if (e.which == 87) {
|
||||
$body.toggleClass('show-wip hide-wip');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSock(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
@@ -49,6 +49,11 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.setElement($("#page-"+this.options.type));
|
||||
// handle single "secondary" action
|
||||
if (this.options.actions && this.options.actions.secondary &&
|
||||
!_.isArray(this.options.actions.secondary)) {
|
||||
this.options.actions.secondary = [this.options.actions.secondary];
|
||||
}
|
||||
return this;
|
||||
},
|
||||
// public API: show() and hide()
|
||||
|
||||
@@ -20,9 +20,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
// because these are outside of this.$el, they can't be in the event hash
|
||||
$('.save-button').on('click', this, this.saveView);
|
||||
$('.cancel-button').on('click', this, this.revertView);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
},
|
||||
render: function() {
|
||||
@@ -45,7 +42,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
|
||||
var policyValues = listEle$.find('.json');
|
||||
_.each(policyValues, this.attachJSONEditor, this);
|
||||
this.showMessage();
|
||||
return this;
|
||||
},
|
||||
attachJSONEditor : function (textarea) {
|
||||
@@ -61,7 +57,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
mode: "application/json", lineNumbers: false, lineWrapping: false,
|
||||
onChange: function(instance, changeobj) {
|
||||
// this event's being called even when there's no change :-(
|
||||
if (instance.getValue() !== oldValue) self.showSaveCancelButtons();
|
||||
if (instance.getValue() !== oldValue && !self.notificationBarShowing) {
|
||||
self.showNotificationBar();
|
||||
}
|
||||
},
|
||||
onFocus : function(mirror) {
|
||||
$(textarea).parent().children('label').addClass("is-focused");
|
||||
@@ -99,59 +97,65 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
});
|
||||
},
|
||||
showMessage: function (type) {
|
||||
$(".wrapper-alert").removeClass("is-shown");
|
||||
if (type) {
|
||||
if (type === this.error_saving) {
|
||||
$(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false');
|
||||
}
|
||||
else if (type === this.successful_changes) {
|
||||
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
|
||||
this.hideSaveCancelButtons();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This is the case of the page first rendering, or when Cancel is pressed.
|
||||
this.hideSaveCancelButtons();
|
||||
showNotificationBar: function() {
|
||||
var self = this;
|
||||
var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.")
|
||||
var confirm = new CMS.Views.Notification.Warning({
|
||||
title: gettext("You've Made Some Changes"),
|
||||
message: message,
|
||||
actions: {
|
||||
primary: {
|
||||
"text": gettext("Save Changes"),
|
||||
"class": "action-save",
|
||||
"click": function() {
|
||||
self.saveView();
|
||||
confirm.hide();
|
||||
self.notificationBarShowing = false;
|
||||
}
|
||||
},
|
||||
secondary: [{
|
||||
"text": gettext("Cancel"),
|
||||
"class": "action-cancel",
|
||||
"click": function() {
|
||||
self.revertView();
|
||||
confirm.hide();
|
||||
self.notificationBarShowing = false;
|
||||
}
|
||||
}]
|
||||
}});
|
||||
this.notificationBarShowing = true;
|
||||
confirm.show();
|
||||
if(this.saved) {
|
||||
this.saved.hide();
|
||||
}
|
||||
},
|
||||
showSaveCancelButtons: function(event) {
|
||||
if (!this.notificationBarShowing) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false');
|
||||
this.notificationBarShowing = true;
|
||||
}
|
||||
},
|
||||
hideSaveCancelButtons: function() {
|
||||
if (this.notificationBarShowing) {
|
||||
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true');
|
||||
this.notificationBarShowing = false;
|
||||
}
|
||||
},
|
||||
saveView : function(event) {
|
||||
window.CmsUtils.smoothScrollTop(event);
|
||||
saveView : function() {
|
||||
// TODO one last verification scan:
|
||||
// call validateKey on each to ensure proper format
|
||||
// check for dupes
|
||||
var self = event.data;
|
||||
self.model.save({},
|
||||
var self = this;
|
||||
this.model.save({},
|
||||
{
|
||||
success : function() {
|
||||
self.render();
|
||||
self.showMessage(self.successful_changes);
|
||||
var message = gettext("Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.");
|
||||
self.saved = new CMS.Views.Alert.Confirmation({
|
||||
title: gettext("Your policy changes have been saved."),
|
||||
message: message,
|
||||
closeIcon: false
|
||||
});
|
||||
self.saved.show();
|
||||
analytics.track('Saved Advanced Settings', {
|
||||
'course': course_location_analytics
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
revertView : function(event) {
|
||||
event.preventDefault();
|
||||
var self = event.data;
|
||||
self.model.deleteKeys = [];
|
||||
self.model.clear({silent : true});
|
||||
self.model.fetch({
|
||||
revertView : function() {
|
||||
var self = this;
|
||||
this.model.deleteKeys = [];
|
||||
this.model.clear({silent : true});
|
||||
this.model.fetch({
|
||||
success : function() { self.render(); },
|
||||
reset: true
|
||||
});
|
||||
|
||||
@@ -61,8 +61,6 @@
|
||||
<div class="wrapper wrapper-view">
|
||||
<%include file="widgets/header.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_alerts"></%block>
|
||||
<div id="page-alert"></div>
|
||||
|
||||
<%block name="content"></%block>
|
||||
@@ -74,13 +72,9 @@
|
||||
<%include file="widgets/footer.html" />
|
||||
<%include file="widgets/tender.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_notifications"></%block>
|
||||
<div id="page-notification"></div>
|
||||
</div>
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_prompts"></%block>
|
||||
<div id="page-prompt"></div>
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
@@ -104,60 +104,3 @@ editor.render();
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_notifications">
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning" aria-hidden="true" role="dialog" aria-labelledby="notification-changesMade-title" aria-describedby="notification-changesMade-description">
|
||||
<div class="notification warning has-actions">
|
||||
<i class="icon-warning-sign"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3" id="notification-changesMade-title">You've Made Some Changes</h2>
|
||||
<p id="notification-changesMade-description">Your changes will not take effect until you <strong>save your progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Notification Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action-primary save-button">Save Changes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="" class="action-secondary cancel-button">Cancel</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="view_alerts">
|
||||
<!-- alert: save confirmed with close -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
|
||||
<div class="alert confirmation">
|
||||
<i class="icon-ok"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">Your policy changes have been saved.</h2>
|
||||
<p>Please note that validation of your policy key and value pairs is not currently in place yet. If you are having difficulties, please review your policy pairs.</p>
|
||||
</div>
|
||||
|
||||
<a href="" rel="view" class="action action-alert-close">
|
||||
<i class="icon-remove-sign"></i>
|
||||
<span class="label">close alert</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- alert: error -->
|
||||
<div class="wrapper wrapper-alert wrapper-alert-error" role="status">
|
||||
<div class="alert error">
|
||||
<i class="icon-warning-sign"></i>
|
||||
|
||||
<div class="copy">
|
||||
<h2 class="title title-3">There was an error saving your information</h2>
|
||||
<p>Please see the error below and correct it to ensure there are no problems in rendering your course.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import string # pylint: disable=W0402
|
||||
import fnmatch
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import *
|
||||
from django.conf.urls import url, patterns
|
||||
|
||||
urlpatterns = patterns('', # nopep8
|
||||
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from student.models import *
|
||||
from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed
|
||||
from student.models import CourseEnrollment, Registration, PendingNameChange
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
admin.site.register(UserProfile)
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ rate -- messages per second
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
global log_file
|
||||
(user_file, message_base, logfilename, ratestr) = args
|
||||
|
||||
users = [u.strip() for u in open(user_file).readlines()]
|
||||
|
||||
@@ -55,11 +55,15 @@ class ReactivationEmailTests(EmailTestMixin, TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create()
|
||||
self.unregisteredUser = UserFactory.create()
|
||||
self.registration = RegistrationFactory.create(user=self.user)
|
||||
|
||||
def reactivation_email(self):
|
||||
"""Send the reactivation email, and return the response as json data"""
|
||||
return json.loads(reactivation_email_for_user(self.user).content)
|
||||
def reactivation_email(self, user):
|
||||
"""
|
||||
Send the reactivation email to the specified user,
|
||||
and return the response as json data.
|
||||
"""
|
||||
return json.loads(reactivation_email_for_user(user).content)
|
||||
|
||||
def assertReactivateEmailSent(self, email_user):
|
||||
"""Assert that the correct reactivation email has been sent"""
|
||||
@@ -78,13 +82,22 @@ class ReactivationEmailTests(EmailTestMixin, TestCase):
|
||||
|
||||
def test_reactivation_email_failure(self, email_user):
|
||||
self.user.email_user.side_effect = Exception
|
||||
response_data = self.reactivation_email()
|
||||
response_data = self.reactivation_email(self.user)
|
||||
|
||||
self.assertReactivateEmailSent(email_user)
|
||||
self.assertFalse(response_data['success'])
|
||||
|
||||
def test_reactivation_for_unregistered_user(self, email_user):
|
||||
"""
|
||||
Test that trying to send a reactivation email to an unregistered
|
||||
user fails without throwing a 500 error.
|
||||
"""
|
||||
response_data = self.reactivation_email(self.unregisteredUser)
|
||||
|
||||
self.assertFalse(response_data['success'])
|
||||
|
||||
def test_reactivation_email_success(self, email_user):
|
||||
response_data = self.reactivation_email()
|
||||
response_data = self.reactivation_email(self.user)
|
||||
|
||||
self.assertReactivateEmailSent(email_user)
|
||||
self.assertTrue(response_data['success'])
|
||||
@@ -150,7 +163,7 @@ class EmailChangeRequestTests(TestCase):
|
||||
self.check_duplicate_email(self.new_email)
|
||||
|
||||
def test_capitalized_duplicate_email(self):
|
||||
raise SkipTest("We currently don't check for emails in a case insensitive way, but we should")
|
||||
"""Test that we check for email addresses in a case insensitive way"""
|
||||
UserFactory.create(email=self.new_email)
|
||||
self.check_duplicate_email(self.new_email.capitalize())
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import string # pylint: disable=W0402
|
||||
import urllib
|
||||
import uuid
|
||||
import time
|
||||
@@ -176,7 +176,7 @@ def _cert_info(user, course, cert_status):
|
||||
CertificateStatuses.downloadable: 'ready',
|
||||
CertificateStatuses.notpassing: 'notpassing',
|
||||
CertificateStatuses.restricted: 'restricted',
|
||||
}
|
||||
}
|
||||
|
||||
status = template_state.get(cert_status['status'], default_status)
|
||||
|
||||
@@ -185,10 +185,10 @@ def _cert_info(user, course, cert_status):
|
||||
'show_disabled_download_button': status == 'generating', }
|
||||
|
||||
if (status in ('generating', 'ready', 'notpassing', 'restricted') and
|
||||
course.end_of_course_survey_url is not None):
|
||||
course.end_of_course_survey_url is not None):
|
||||
d.update({
|
||||
'show_survey_button': True,
|
||||
'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
|
||||
'show_survey_button': True,
|
||||
'survey_url': process_survey_link(course.end_of_course_survey_url, user)})
|
||||
else:
|
||||
d['show_survey_button'] = False
|
||||
|
||||
@@ -913,8 +913,8 @@ def get_random_post_override():
|
||||
'password': id_generator(),
|
||||
'name': (id_generator(size=5, chars=string.ascii_lowercase) + " " +
|
||||
id_generator(size=7, chars=string.ascii_lowercase)),
|
||||
'honor_code': u'true',
|
||||
'terms_of_service': u'true', }
|
||||
'honor_code': u'true',
|
||||
'terms_of_service': u'true', }
|
||||
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
@@ -985,21 +985,12 @@ def password_reset(request):
|
||||
'error': 'Invalid e-mail'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def reactivation_email(request):
|
||||
''' Send an e-mail to reactivate a deactivated account, or to
|
||||
resend an activation e-mail. Untested. '''
|
||||
email = request.POST['email']
|
||||
def reactivation_email_for_user(user):
|
||||
try:
|
||||
user = User.objects.get(email='email')
|
||||
except User.DoesNotExist:
|
||||
reg = Registration.objects.get(user=user)
|
||||
except Registration.DoesNotExist:
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'No inactive user with this e-mail exists'}))
|
||||
return reactivation_email_for_user(user)
|
||||
|
||||
|
||||
def reactivation_email_for_user(user):
|
||||
reg = Registration.objects.get(user=user)
|
||||
|
||||
d = {'name': user.profile.name,
|
||||
'key': reg.activation_key}
|
||||
|
||||
@@ -10,10 +10,9 @@ 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.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from bs4 import BeautifulSoup
|
||||
import os.path
|
||||
from urllib import quote_plus
|
||||
|
||||
|
||||
@@ -75,51 +74,6 @@ def register_by_course_id(course_id, is_staff=False):
|
||||
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
|
||||
@@ -129,6 +83,6 @@ def clear_courses():
|
||||
# (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(modulestore('direct'))
|
||||
contentstore().fs_files.drop()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from track.models import *
|
||||
from track.models import TrackingLog
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(TrackingLog)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Provides sympy representation.
|
||||
|
||||
import os
|
||||
import string
|
||||
import string # pylint: disable=W0402
|
||||
import re
|
||||
import logging
|
||||
import operator
|
||||
|
||||
@@ -18,8 +18,6 @@ def load_function(path):
|
||||
|
||||
|
||||
def contentstore(name='default'):
|
||||
global _CONTENTSTORE
|
||||
|
||||
if name not in _CONTENTSTORE:
|
||||
class_ = load_function(settings.CONTENTSTORE['ENGINE'])
|
||||
options = {}
|
||||
|
||||
@@ -26,8 +26,6 @@ def load_function(path):
|
||||
|
||||
|
||||
def modulestore(name='default'):
|
||||
global _MODULESTORES
|
||||
|
||||
if name not in _MODULESTORES:
|
||||
class_ = load_function(settings.MODULESTORE[name]['ENGINE'])
|
||||
|
||||
|
||||
1
common/test/data/uploads/test
Normal file
1
common/test/data/uploads/test
Normal file
@@ -0,0 +1 @@
|
||||
R2FUIGM88K
|
||||
@@ -58,21 +58,24 @@ In the discussion service, notifications are handled asynchronously using a thir
|
||||
|
||||
bundle exec rake jobs:work
|
||||
|
||||
## Initialize roles and permissions
|
||||
## From the edx-platform django app, initialize roles and permissions
|
||||
|
||||
To fully test the discussion forum, you might want to act as a moderator or an administrator. Currently, moderators can manage everything in the forum, and administrator can manage everything plus assigning and revoking moderator status of other users.
|
||||
|
||||
First make sure that the database is up-to-date:
|
||||
|
||||
rake django-admin[syncdb]
|
||||
rake django-admin[migrate]
|
||||
rake resetdb
|
||||
|
||||
If you have created users in the edx-platform django apps when the comment service was not running, you will need to one-way sync the users into the comment service back end database:
|
||||
|
||||
rake django-admin[sync_user_info]
|
||||
|
||||
For convenience, add the following environment variables to the terminal (assuming that you're using configuration set lms.envs.dev):
|
||||
|
||||
export DJANGO_SETTINGS_MODULE=lms.envs.dev
|
||||
export PYTHONPATH=.
|
||||
|
||||
Now initialzie roles and permissions, providing a course id eg.:
|
||||
Now initialize roles and permissions, providing a course id. See the example below. Note that you do not need to do this for Studio-created courses, as the Studio application does this for you.
|
||||
|
||||
django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from courseware.models import *
|
||||
from courseware.models import StudentModule, OfflineComputedGrade, OfflineComputedGradeLog
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
# Here are all the courses for Fall 2012
|
||||
# MITx/3.091x/2012_Fall
|
||||
# MITx/6.002x/2012_Fall
|
||||
# MITx/6.00x/2012_Fall
|
||||
# HarvardX/CS50x/2012 (we will not be testing this, as it is anomolistic)
|
||||
# HarvardX/PH207x/2012_Fall
|
||||
# BerkeleyX/CS169.1x/2012_Fall
|
||||
# BerkeleyX/CS169.2x/2012_Fall
|
||||
# BerkeleyX/CS184.1x/2012_Fall
|
||||
|
||||
#You can load the courses into your data directory with these cmds:
|
||||
# git clone https://github.com/MITx/3.091x.git
|
||||
# git clone https://github.com/MITx/6.00x.git
|
||||
# git clone https://github.com/MITx/content-mit-6002x.git
|
||||
# git clone https://github.com/MITx/content-mit-6002x.git
|
||||
# git clone https://github.com/MITx/content-harvard-id270x.git
|
||||
# git clone https://github.com/MITx/content-berkeley-cs169x.git
|
||||
# git clone https://github.com/MITx/content-berkeley-cs169.2x.git
|
||||
# git clone https://github.com/MITx/content-berkeley-cs184x.git
|
||||
|
||||
Feature: There are courses on the homepage
|
||||
In order to compared rendered content to the database
|
||||
As an acceptance test
|
||||
I want to count all the chapters, sections, and tabs for each course
|
||||
|
||||
# Commenting these all out for now because they don't always run,
|
||||
# they have too many prerequesites, e.g. the course exists, and
|
||||
# is within the start and end dates, etc.
|
||||
|
||||
# Scenario: Navigate through course MITx/3.091x/2012_Fall
|
||||
# Given I am registered for course "MITx/3.091x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course MITx/6.002x/2012_Fall
|
||||
# Given I am registered for course "MITx/6.002x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course MITx/6.00x/2012_Fall
|
||||
# Given I am registered for course "MITx/6.00x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course HarvardX/PH207x/2012_Fall
|
||||
# Given I am registered for course "HarvardX/PH207x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course BerkeleyX/CS169.1x/2012_Fall
|
||||
# Given I am registered for course "BerkeleyX/CS169.1x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course BerkeleyX/CS169.2x/2012_Fall
|
||||
# Given I am registered for course "BerkeleyX/CS169.2x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
|
||||
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
|
||||
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
|
||||
# And I log in
|
||||
# Then I verify all the content of each course
|
||||
@@ -1,158 +0,0 @@
|
||||
#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 common import *
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def check_for_errors():
|
||||
e = world.browser.find_by_css('.outside-app')
|
||||
if len(e) > 0:
|
||||
assert False, 'there was a server error at %s' % (world.browser.url)
|
||||
else:
|
||||
assert True
|
||||
|
||||
|
||||
@step(u'I verify all the content of each course')
|
||||
def i_verify_all_the_content_of_each_course(step):
|
||||
all_possible_courses = get_courses()
|
||||
logger.debug('Courses found:')
|
||||
for c in all_possible_courses:
|
||||
logger.debug(c.id)
|
||||
ids = [c.id for c in all_possible_courses]
|
||||
|
||||
# Get a list of all the registered courses
|
||||
registered_courses = world.browser.find_by_css('article.my-course')
|
||||
if len(all_possible_courses) < len(registered_courses):
|
||||
assert False, "user is registered for more courses than are uniquely posssible"
|
||||
else:
|
||||
pass
|
||||
|
||||
for test_course in registered_courses:
|
||||
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.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.css_click('.user-link')
|
||||
check_for_errors()
|
||||
|
||||
|
||||
def browse_course(course_id):
|
||||
|
||||
## count chapters from xml and page and compare
|
||||
chapters = get_courseware_with_tabs(course_id)
|
||||
num_chapters = len(chapters)
|
||||
|
||||
rendered_chapters = world.browser.find_by_css('#accordion > nav > div')
|
||||
num_rendered_chapters = len(rendered_chapters)
|
||||
|
||||
msg = '%d chapters expected, %d chapters found on page for %s' % (num_chapters, num_rendered_chapters, course_id)
|
||||
#logger.debug(msg)
|
||||
assert num_chapters == num_rendered_chapters, msg
|
||||
|
||||
chapter_it = 0
|
||||
|
||||
## Iterate the chapters
|
||||
while chapter_it < num_chapters:
|
||||
|
||||
## click into a chapter
|
||||
world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('h3').click()
|
||||
|
||||
## look for the "there was a server error" div
|
||||
check_for_errors()
|
||||
|
||||
## count sections from xml and page and compare
|
||||
sections = chapters[chapter_it]['sections']
|
||||
num_sections = len(sections)
|
||||
|
||||
rendered_sections = world.browser.find_by_css('#accordion > nav > div')[chapter_it].find_by_tag('li')
|
||||
num_rendered_sections = len(rendered_sections)
|
||||
|
||||
msg = ('%d sections expected, %d sections found on page, %s - %d - %s' %
|
||||
(num_sections, num_rendered_sections, course_id, chapter_it, chapters[chapter_it]['chapter_name']))
|
||||
#logger.debug(msg)
|
||||
assert num_sections == num_rendered_sections, msg
|
||||
|
||||
section_it = 0
|
||||
|
||||
## Iterate the sections
|
||||
while section_it < num_sections:
|
||||
|
||||
## click on a section
|
||||
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.is_css_present('.course-content')
|
||||
|
||||
## look for server error div
|
||||
check_for_errors()
|
||||
|
||||
## count tabs from xml and page and compare
|
||||
|
||||
## count the number of tabs. If number of tabs is 0, there won't be anything rendered
|
||||
## so we explicitly set rendered_tabs because otherwise find_elements returns a None object with no length
|
||||
num_tabs = sections[section_it]['clickable_tab_count']
|
||||
if num_tabs != 0:
|
||||
rendered_tabs = world.browser.find_by_css('ol#sequence-list > li')
|
||||
num_rendered_tabs = len(rendered_tabs)
|
||||
else:
|
||||
rendered_tabs = 0
|
||||
num_rendered_tabs = 0
|
||||
|
||||
msg = ('%d tabs expected, %d tabs found, %s - %d - %s' %
|
||||
(num_tabs, num_rendered_tabs, course_id, section_it, sections[section_it]['section_name']))
|
||||
#logger.debug(msg)
|
||||
|
||||
# Save the HTML to a file for later comparison
|
||||
world.save_the_course_content('/tmp/%s' % course_id)
|
||||
|
||||
assert num_tabs == num_rendered_tabs, msg
|
||||
|
||||
tabs = sections[section_it]['tabs']
|
||||
tab_it = 0
|
||||
|
||||
## Iterate the tabs
|
||||
while tab_it < num_tabs:
|
||||
|
||||
rendered_tabs[tab_it].find_by_tag('a').click()
|
||||
|
||||
## do something with the tab sections[section_it]
|
||||
# e = world.browser.find_by_css('section.course-content section')
|
||||
# process_section(e)
|
||||
tab_children = tabs[tab_it]['children_count']
|
||||
tab_class = tabs[tab_it]['class']
|
||||
if tab_children != 0:
|
||||
rendered_items = world.browser.find_by_css('div#seq_content > section > ol > li > section')
|
||||
num_rendered_items = len(rendered_items)
|
||||
msg = ('%d items expected, %d items found, %s - %d - %s - tab %d' %
|
||||
(tab_children, num_rendered_items, course_id, section_it, sections[section_it]['section_name'], tab_it))
|
||||
#logger.debug(msg)
|
||||
assert tab_children == num_rendered_items, msg
|
||||
|
||||
tab_it += 1
|
||||
|
||||
section_it += 1
|
||||
|
||||
chapter_it += 1
|
||||
|
||||
|
||||
def validate_course(current_course, ids):
|
||||
try:
|
||||
ids.index(current_course)
|
||||
except:
|
||||
assert False, "invalid course id %s" % current_course
|
||||
@@ -2,7 +2,7 @@ from django.conf import settings
|
||||
from .mustache_helpers import mustache_helpers
|
||||
from functools import partial
|
||||
|
||||
from .utils import *
|
||||
from .utils import extend_content, merge_dict, render_mustache
|
||||
import django_comment_client.settings as cc_settings
|
||||
|
||||
import pystache_custom as pystache
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import string
|
||||
import string # pylint: disable=W0402
|
||||
import random
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@@ -73,21 +73,17 @@ def get_discussion_id_map(course):
|
||||
"""
|
||||
return a dict of the form {category: modules}
|
||||
"""
|
||||
global _DISCUSSIONINFO
|
||||
initialize_discussion_info(course)
|
||||
return _DISCUSSIONINFO[course.id]['id_map']
|
||||
|
||||
|
||||
def get_discussion_title(course, discussion_id):
|
||||
global _DISCUSSIONINFO
|
||||
initialize_discussion_info(course)
|
||||
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
|
||||
return title
|
||||
|
||||
|
||||
def get_discussion_category_map(course):
|
||||
|
||||
global _DISCUSSIONINFO
|
||||
initialize_discussion_info(course)
|
||||
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
|
||||
|
||||
@@ -141,8 +137,6 @@ def sort_map_entries(category_map):
|
||||
|
||||
|
||||
def initialize_discussion_info(course):
|
||||
global _DISCUSSIONINFO
|
||||
|
||||
course_id = course.id
|
||||
|
||||
discussion_id_map = {}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# django management command: dump grades to csv files
|
||||
# for use by batch processes
|
||||
|
||||
from instructor.offline_gradecalc import *
|
||||
from instructor.offline_gradecalc import offline_grade_calculation
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore
|
||||
|
||||
USER_COUNT = 11
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestGradebook(ModuleStoreTestCase):
|
||||
grading_policy = None
|
||||
@@ -41,10 +42,7 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
metadata={'graded': True, 'format': 'Homework'}
|
||||
)
|
||||
|
||||
self.users = [
|
||||
UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i)
|
||||
for i in xrange(USER_COUNT)
|
||||
]
|
||||
self.users = [UserFactory() for _ in xrange(USER_COUNT)]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
@@ -72,10 +70,11 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
def test_response_code(self):
|
||||
self.assertEquals(self.response.status_code, 200)
|
||||
|
||||
|
||||
class TestDefaultGradingPolicy(TestGradebook):
|
||||
def test_all_users_listed(self):
|
||||
for user in self.users:
|
||||
self.assertIn(user.username, self.response.content)
|
||||
self.assertIn(user.username, unicode(self.response.content, 'utf-8'))
|
||||
|
||||
def test_default_policy(self):
|
||||
# Default >= 50% passes, so Users 5-10 should be passing for Homework 1 [6]
|
||||
@@ -92,6 +91,7 @@ class TestDefaultGradingPolicy(TestGradebook):
|
||||
# One use at the top of the page [1]
|
||||
self.assertEquals(293, self.response.content.count('grade_None'))
|
||||
|
||||
|
||||
class TestLetterCutoffPolicy(TestGradebook):
|
||||
grading_policy = {
|
||||
"GRADER": [
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import string
|
||||
import string # pylint: disable=W0402
|
||||
import datetime
|
||||
from getpass import getpass
|
||||
import json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
django admin pages for courseware model
|
||||
'''
|
||||
|
||||
from psychometrics.models import *
|
||||
from psychometrics.models import PsychometricData
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(PsychometricData)
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import json
|
||||
|
||||
from courseware.models import *
|
||||
from track.models import *
|
||||
from psychometrics.models import *
|
||||
from courseware.models import StudentModule
|
||||
from track.models import TrackingLog
|
||||
from psychometrics.models import PsychometricData
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -14,7 +14,8 @@ from scipy.optimize import curve_fit
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Sum, Max
|
||||
from psychometrics.models import *
|
||||
from psychometrics.models import PsychometricData
|
||||
from courseware.models import StudentModule
|
||||
from pytz import UTC
|
||||
|
||||
log = logging.getLogger("mitx.psychometrics")
|
||||
@@ -303,7 +304,7 @@ def generate_plots_for_problem(problem):
|
||||
def make_psychometrics_data_update_handler(course_id, user, module_state_key):
|
||||
"""
|
||||
Construct and return a procedure which may be called to update
|
||||
the PsychometricsData instance for the given StudentModule instance.
|
||||
the PsychometricData instance for the given StudentModule instance.
|
||||
"""
|
||||
sm, status = StudentModule.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
|
||||
@@ -103,6 +103,13 @@ MITX_FEATURES = {
|
||||
# analytics experiments
|
||||
'ENABLE_INSTRUCTOR_ANALYTICS': False,
|
||||
|
||||
# enable analytics server.
|
||||
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
|
||||
# LMS OPERATION. See analytics.py for details about what
|
||||
# this does.
|
||||
|
||||
'RUN_AS_ANALYTICS_SERVER_ENABLED' : False,
|
||||
|
||||
# Flip to True when the YouTube iframe API breaks (again)
|
||||
'USE_YOUTUBE_OBJECT_API': False,
|
||||
|
||||
|
||||
@@ -258,6 +258,6 @@ if SEGMENT_IO_LMS_KEY:
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
from .private import *
|
||||
from .private import * # pylint: disable=F0401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .utils import *
|
||||
from .utils import CommentClientError, perform_request
|
||||
|
||||
from .thread import Thread
|
||||
from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread
|
||||
import models
|
||||
import settings
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from .thread import Thread
|
||||
from .user import User
|
||||
from .commentable import Commentable
|
||||
|
||||
from .utils import *
|
||||
from .utils import perform_request
|
||||
|
||||
import settings
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from .utils import *
|
||||
|
||||
import models
|
||||
import settings
|
||||
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
def delete_threads(commentable_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
|
||||
|
||||
|
||||
def get_threads(commentable_id, recursive=False, query_params={}, *args, **kwargs):
|
||||
default_params = {'page': 1, 'per_page': 20, 'recursive': recursive}
|
||||
attributes = dict(default_params.items() + query_params.items())
|
||||
response = _perform_request('get', _url_for_threads(commentable_id), attributes, *args, **kwargs)
|
||||
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
|
||||
|
||||
|
||||
def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
|
||||
default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive}
|
||||
attributes = dict(default_params.items() + query_params.items())
|
||||
response = _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
|
||||
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
|
||||
|
||||
|
||||
def search_similar_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
|
||||
default_params = {'course_id': course_id, 'recursive': recursive}
|
||||
attributes = dict(default_params.items() + query_params.items())
|
||||
return _perform_request('get', _url_for_search_similar_threads(), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def search_recent_active_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
|
||||
default_params = {'course_id': course_id, 'recursive': recursive}
|
||||
attributes = dict(default_params.items() + query_params.items())
|
||||
return _perform_request('get', _url_for_search_recent_active_threads(), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def search_trending_tags(course_id, query_params={}, *args, **kwargs):
|
||||
default_params = {'course_id': course_id}
|
||||
attributes = dict(default_params.items() + query_params.items())
|
||||
return _perform_request('get', _url_for_search_trending_tags(), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def create_user(attributes, *args, **kwargs):
|
||||
return _perform_request('post', _url_for_users(), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def update_user(user_id, attributes, *args, **kwargs):
|
||||
return _perform_request('put', _url_for_user(user_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def get_threads_tags(*args, **kwargs):
|
||||
return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
|
||||
|
||||
|
||||
def tags_autocomplete(value, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs)
|
||||
|
||||
|
||||
def create_thread(commentable_id, attributes, *args, **kwargs):
|
||||
return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def get_thread(thread_id, recursive=False, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs)
|
||||
|
||||
|
||||
def update_thread(thread_id, attributes, *args, **kwargs):
|
||||
return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def create_comment(thread_id, attributes, *args, **kwargs):
|
||||
return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def delete_thread(thread_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs)
|
||||
|
||||
|
||||
def get_comment(comment_id, recursive=False, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs)
|
||||
|
||||
|
||||
def update_comment(comment_id, attributes, *args, **kwargs):
|
||||
return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def create_sub_comment(comment_id, attributes, *args, **kwargs):
|
||||
return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs)
|
||||
|
||||
|
||||
def delete_comment(comment_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs)
|
||||
|
||||
|
||||
def vote_for_comment(comment_id, user_id, value, *args, **kwargs):
|
||||
return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
|
||||
|
||||
|
||||
def undo_vote_for_comment(comment_id, user_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_vote_comment(comment_id), {'user_id': user_id}, *args, **kwargs)
|
||||
|
||||
|
||||
def vote_for_thread(thread_id, user_id, value, *args, **kwargs):
|
||||
return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
|
||||
|
||||
|
||||
def undo_vote_for_thread(thread_id, user_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_vote_thread(thread_id), {'user_id': user_id}, *args, **kwargs)
|
||||
|
||||
|
||||
def get_notifications(user_id, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs)
|
||||
|
||||
|
||||
def get_user_info(user_id, complete=True, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs)
|
||||
|
||||
|
||||
def subscribe(user_id, subscription_detail, *args, **kwargs):
|
||||
return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
|
||||
|
||||
|
||||
def subscribe_user(user_id, followed_user_id, *args, **kwargs):
|
||||
return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
|
||||
|
||||
follow = subscribe_user
|
||||
|
||||
|
||||
def subscribe_thread(user_id, thread_id, *args, **kwargs):
|
||||
return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
|
||||
|
||||
|
||||
def subscribe_commentable(user_id, commentable_id, *args, **kwargs):
|
||||
return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
|
||||
|
||||
|
||||
def unsubscribe(user_id, subscription_detail, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
|
||||
|
||||
|
||||
def unsubscribe_user(user_id, followed_user_id, *args, **kwargs):
|
||||
return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
|
||||
|
||||
unfollow = unsubscribe_user
|
||||
|
||||
|
||||
def unsubscribe_thread(user_id, thread_id, *args, **kwargs):
|
||||
return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
|
||||
|
||||
|
||||
def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
|
||||
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
|
||||
|
||||
|
||||
def _perform_request(method, url, data_or_params=None, *args, **kwargs):
|
||||
if method in ['post', 'put', 'patch']:
|
||||
response = requests.request(method, url, data=data_or_params)
|
||||
else:
|
||||
response = requests.request(method, url, params=data_or_params)
|
||||
if 200 < response.status_code < 500:
|
||||
raise CommentClientError(response.text)
|
||||
elif response.status_code == 500:
|
||||
raise CommentClientUnknownError(response.text)
|
||||
else:
|
||||
if kwargs.get("raw", False):
|
||||
return response.text
|
||||
else:
|
||||
return json.loads(response.text)
|
||||
|
||||
|
||||
def _url_for_threads(commentable_id):
|
||||
return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id)
|
||||
|
||||
|
||||
def _url_for_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_thread_comments(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_comment(comment_id):
|
||||
return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id)
|
||||
|
||||
|
||||
def _url_for_vote_comment(comment_id):
|
||||
return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id)
|
||||
|
||||
|
||||
def _url_for_vote_thread(thread_id):
|
||||
return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id)
|
||||
|
||||
|
||||
def _url_for_notifications(user_id):
|
||||
return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id)
|
||||
|
||||
|
||||
def _url_for_subscription(user_id):
|
||||
return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id)
|
||||
|
||||
|
||||
def _url_for_user(user_id):
|
||||
return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id)
|
||||
|
||||
|
||||
def _url_for_search_threads():
|
||||
return "{prefix}/search/threads".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_search_similar_threads():
|
||||
return "{prefix}/search/threads/more_like_this".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_search_recent_active_threads():
|
||||
return "{prefix}/search/threads/recent_active".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_search_trending_tags():
|
||||
return "{prefix}/search/tags/trending".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_threads_tags():
|
||||
return "{prefix}/threads/tags".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_threads_tags_autocomplete():
|
||||
return "{prefix}/threads/tags/autocomplete".format(prefix=PREFIX)
|
||||
|
||||
|
||||
def _url_for_users():
|
||||
return "{prefix}/users".format(prefix=PREFIX)
|
||||
@@ -1,4 +1,5 @@
|
||||
from .utils import *
|
||||
from .utils import merge_dict, strip_blank, strip_none, extract, perform_request
|
||||
from .utils import CommentClientError
|
||||
import models
|
||||
import settings
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .utils import *
|
||||
from .utils import merge_dict, perform_request, CommentClientError
|
||||
|
||||
import models
|
||||
import settings
|
||||
|
||||
@@ -46,6 +46,13 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
form.choicegroup {
|
||||
label {
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
textarea,
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
|
||||
10
lms/urls.py
10
lms/urls.py
@@ -116,8 +116,6 @@ if not settings.MITX_FEATURES["USE_CUSTOM_THEME"]:
|
||||
|
||||
url(r'^submit_feedback$', 'util.views.submit_feedback'),
|
||||
|
||||
# TODO: These urls no longer work. They need to be updated before they are re-enabled
|
||||
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
|
||||
)
|
||||
|
||||
# Only enable URLs for those marketing links actually enabled in the
|
||||
@@ -415,6 +413,12 @@ if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
|
||||
url(r'^instructor_task_status/$', 'instructor_task.views.instructor_task_status', name='instructor_task_status'),
|
||||
)
|
||||
|
||||
if settings.MITX_FEATURES.get('RUN_AS_ANALYTICS_SERVER_ENABLED'):
|
||||
urlpatterns += (
|
||||
url(r'^edinsights_service/', include('edinsights.core.urls')),
|
||||
)
|
||||
import edinsights.core.registry
|
||||
|
||||
# FoldIt views
|
||||
urlpatterns += (
|
||||
# The path is hardcoded into their app...
|
||||
@@ -434,3 +438,5 @@ if settings.DEBUG:
|
||||
#Custom error pages
|
||||
handler404 = 'static_template_view.views.render_404'
|
||||
handler500 = 'static_template_view.views.render_500'
|
||||
|
||||
|
||||
|
||||
4
pylintrc
4
pylintrc
@@ -41,6 +41,10 @@ disable=
|
||||
# W0142: Used * or ** magic
|
||||
I0011,C0301,W0141,W0142,
|
||||
|
||||
# Django makes classes that trigger these
|
||||
# W0232: Class has no __init__ method
|
||||
W0232,
|
||||
|
||||
# Might use these when the code is in better shape
|
||||
# C0302: Too many lines in module
|
||||
# R0201: Method could be a function
|
||||
|
||||
2
rakefile
2
rakefile
@@ -3,7 +3,7 @@ begin
|
||||
require 'rake/clean'
|
||||
require './rakelib/helpers.rb'
|
||||
rescue LoadError => error
|
||||
puts "Import faild (#{error})"
|
||||
puts "Import failed (#{error})"
|
||||
puts "Please run `bundle install` to bootstrap ruby dependencies"
|
||||
exit 1
|
||||
end
|
||||
|
||||
@@ -96,13 +96,27 @@ clone_repos() {
|
||||
fi
|
||||
}
|
||||
|
||||
set_base_default() { # if PROJECT_HOME not set
|
||||
# 2 possibilities: this is from cloned repo, or not
|
||||
# this script is in "./scripts" if a git clone
|
||||
this_repo=$(cd "${BASH_SOURCE%/*}/.." && pwd)
|
||||
if [[ "${this_repo##*/}" = "edx-platform" && -d "$this_repo/.git" ]]; then
|
||||
# set BASE one-up from this_repo;
|
||||
echo "${this_repo%/*}"
|
||||
else
|
||||
echo "$HOME/edx_all"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
### START
|
||||
|
||||
PROG=${0##*/}
|
||||
|
||||
# Adjust this to wherever you'd like to place the codebase
|
||||
BASE="${PROJECT_HOME:-$HOME}/edx_all"
|
||||
BASE="${PROJECT_HOME:-$(set_base_default)}"
|
||||
|
||||
# Use a sensible default (~/.virtualenvs) for your Python virtualenvs
|
||||
# unless you've already got one set up with virtualenvwrapper.
|
||||
|
||||
Reference in New Issue
Block a user