diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0e161e4f72..3dda49928b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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. diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 2360baea5a..1661e1c391 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -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). diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index e0f20d3d6e..bdf07fc5ae 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -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' diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature new file mode 100644 index 0000000000..fc1212f398 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-team.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py new file mode 100644 index 0000000000..c126773db6 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -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) diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature new file mode 100644 index 0000000000..81714c43ae --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-updates.feature @@ -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 "
    Test
" + Then I see the handout "Test" diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py new file mode 100644 index 0000000000..d838061698 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -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) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index a3e838a9d1..5b279d402f 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -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$') diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature new file mode 100644 index 0000000000..9997df69f0 --- /dev/null +++ b/cms/djangoapps/contentstore/features/static-pages.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py new file mode 100644 index 0000000000..a16a3246da --- /dev/null +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -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 diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 468099f417..1fbd965871 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -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) diff --git a/cms/djangoapps/contentstore/features/upload.feature b/cms/djangoapps/contentstore/features/upload.feature new file mode 100644 index 0000000000..b3c1fc2ce3 --- /dev/null +++ b/cms/djangoapps/contentstore/features/upload.feature @@ -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 diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py new file mode 100644 index 0000000000..258fc5ebcf --- /dev/null +++ b/cms/djangoapps/contentstore/features/upload.py @@ -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) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 07630bdf31..2dcb3640ca 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -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 diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee index a3950c0b3c..e5916c5ed3 100644 --- a/cms/static/coffee/spec/views/feedback_spec.coffee +++ b/cms/static/coffee/spec/views/feedback_spec.coffee @@ -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') diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 92a16b8417..d1cffdc427 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -25,7 +25,6 @@ $(document).ready(function() { $newComponentTemplatePickers = $('.new-component-templates'); $newComponentButton = $('.new-component-button'); $spinner = $(''); - $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(); diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index 0cfd6fa4ef..3f161d5b1f 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -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() diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 863393d341..302a918de1 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -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 }); diff --git a/cms/templates/base.html b/cms/templates/base.html index 11e8d41496..695a97f1da 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -61,8 +61,6 @@
<%include file="widgets/header.html" /> - ## remove this block after advanced settings notification is rewritten - <%block name="view_alerts">
<%block name="content"> @@ -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">
- ## remove this block after advanced settings notification is rewritten - <%block name="view_prompts">
<%block name="jsextra"> diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 242148418e..6cc3468590 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -104,60 +104,3 @@ editor.render(); - -<%block name="view_notifications"> - - - - -<%block name="view_alerts"> - -
-
- - -
-

Your policy changes have been saved.

-

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.

-
- - - - close alert - -
-
- - -
-
- - -
-

There was an error saving your information

-

Please see the error below and correct it to ensure there are no problems in rendering your course.

-
-
-
- diff --git a/common/djangoapps/external_auth/views.py b/common/djangoapps/external_auth/views.py index 93ab70debb..06709eff9e 100644 --- a/common/djangoapps/external_auth/views.py +++ b/common/djangoapps/external_auth/views.py @@ -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 diff --git a/common/djangoapps/heartbeat/urls.py b/common/djangoapps/heartbeat/urls.py index 3f45a95dd2..6a0be757c9 100644 --- a/common/djangoapps/heartbeat/urls.py +++ b/common/djangoapps/heartbeat/urls.py @@ -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'), diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 64fe844801..4d6976d7d4 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -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) diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index fec354e974..ae25430a85 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -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()] diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py index 3b31bb5c28..7e2d9ede00 100644 --- a/common/djangoapps/student/tests/test_email.py +++ b/common/djangoapps/student/tests/test_email.py @@ -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()) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 02c66b19b0..82c17c0e67 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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} diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index fbc9409e7b..7da49e6315 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -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() diff --git a/common/djangoapps/track/admin.py b/common/djangoapps/track/admin.py index 1f19c59a93..d75f206846 100644 --- a/common/djangoapps/track/admin.py +++ b/common/djangoapps/track/admin.py @@ -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) diff --git a/common/lib/symmath/symmath/formula.py b/common/lib/symmath/symmath/formula.py index ca4e20ace3..d5b97a2550 100644 --- a/common/lib/symmath/symmath/formula.py +++ b/common/lib/symmath/symmath/formula.py @@ -10,7 +10,7 @@ # Provides sympy representation. import os -import string +import string # pylint: disable=W0402 import re import logging import operator diff --git a/common/lib/xmodule/xmodule/contentstore/django.py b/common/lib/xmodule/xmodule/contentstore/django.py index f163348cc8..25a5d7912f 100644 --- a/common/lib/xmodule/xmodule/contentstore/django.py +++ b/common/lib/xmodule/xmodule/contentstore/django.py @@ -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 = {} diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index a2e2a4a5a5..c98e6cadef 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -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']) diff --git a/common/test/data/uploads/test b/common/test/data/uploads/test new file mode 100644 index 0000000000..f019db7176 --- /dev/null +++ b/common/test/data/uploads/test @@ -0,0 +1 @@ +R2FUIGM88K \ No newline at end of file diff --git a/doc/discussion.md b/doc/discussion.md index 2446485497..752dc6a5e7 100644 --- a/doc/discussion.md +++ b/doc/discussion.md @@ -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" diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py index 9ef4c1de20..743d1fed52 100644 --- a/lms/djangoapps/courseware/admin.py +++ b/lms/djangoapps/courseware/admin.py @@ -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 diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature deleted file mode 100644 index fc51eca25d..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.feature +++ /dev/null @@ -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 diff --git a/lms/djangoapps/courseware/features/smart-accordion.py b/lms/djangoapps/courseware/features/smart-accordion.py deleted file mode 100644 index 63408d7683..0000000000 --- a/lms/djangoapps/courseware/features/smart-accordion.py +++ /dev/null @@ -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 diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py index a8a51ad95c..1310c4e0c1 100644 --- a/lms/djangoapps/django_comment_client/helpers.py +++ b/lms/djangoapps/django_comment_client/helpers.py @@ -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 diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py index 8fd8ed7e2b..8c6a48d8c1 100644 --- a/lms/djangoapps/django_comment_client/tests.py +++ b/lms/djangoapps/django_comment_client/tests.py @@ -1,4 +1,4 @@ -import string +import string # pylint: disable=W0402 import random from django.contrib.auth.models import User diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 496c834950..6668826b67 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -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 = {} diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 4518450e39..d1c66d51d2 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -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 diff --git a/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 3d0a1b09b8..bbdf07f410 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -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": [ diff --git a/lms/djangoapps/lms_migration/management/commands/create_user.py b/lms/djangoapps/lms_migration/management/commands/create_user.py index 87abf4f73a..5d96d96a8a 100644 --- a/lms/djangoapps/lms_migration/management/commands/create_user.py +++ b/lms/djangoapps/lms_migration/management/commands/create_user.py @@ -6,7 +6,7 @@ import os import sys -import string +import string # pylint: disable=W0402 import datetime from getpass import getpass import json diff --git a/lms/djangoapps/psychometrics/admin.py b/lms/djangoapps/psychometrics/admin.py index ff1a14d722..b7c04b5069 100644 --- a/lms/djangoapps/psychometrics/admin.py +++ b/lms/djangoapps/psychometrics/admin.py @@ -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) diff --git a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py index 87e62f4a2c..f9cfbd28f5 100644 --- a/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py +++ b/lms/djangoapps/psychometrics/management/commands/init_psychometrics.py @@ -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 diff --git a/lms/djangoapps/psychometrics/psychoanalyze.py b/lms/djangoapps/psychometrics/psychoanalyze.py index ab9a5e6242..c6e66445a4 100644 --- a/lms/djangoapps/psychometrics/psychoanalyze.py +++ b/lms/djangoapps/psychometrics/psychoanalyze.py @@ -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, diff --git a/lms/envs/common.py b/lms/envs/common.py index eff174b3d7..141bc127be 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index b1519b77bc..813f9cf32c 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -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 diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index fb5a4ad0c3..fd68d5cdeb 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -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 diff --git a/lms/lib/comment_client/comment_client.py b/lms/lib/comment_client/comment_client.py index d91c5ea47f..4f660533f1 100644 --- a/lms/lib/comment_client/comment_client.py +++ b/lms/lib/comment_client/comment_client.py @@ -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 diff --git a/lms/lib/comment_client/commentable.py b/lms/lib/comment_client/commentable.py index 111809f8f0..05efd70e50 100644 --- a/lms/lib/comment_client/commentable.py +++ b/lms/lib/comment_client/commentable.py @@ -1,5 +1,3 @@ -from .utils import * - import models import settings diff --git a/lms/lib/comment_client/legacy.py b/lms/lib/comment_client/legacy.py deleted file mode 100644 index de7ce201ce..0000000000 --- a/lms/lib/comment_client/legacy.py +++ /dev/null @@ -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) diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 0b0be576b8..00d5f01814 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -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 diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index a9e47fe6aa..2370052d90 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -1,4 +1,4 @@ -from .utils import * +from .utils import merge_dict, perform_request, CommentClientError import models import settings diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index 6d87b7f554..a1c948d4f5 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -46,6 +46,13 @@ form { } } +form.choicegroup { + label { + clear: both; + float: left; + } +} + textarea, input[type="text"], input[type="email"], diff --git a/lms/urls.py b/lms/urls.py index 922031fe93..96a00d7cd2 100644 --- a/lms/urls.py +++ b/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[^/]*)$', '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' + + diff --git a/pylintrc b/pylintrc index af958e4af4..dea0f240c6 100644 --- a/pylintrc +++ b/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 diff --git a/rakefile b/rakefile index 96bd4c2e96..2cf442bca9 100644 --- a/rakefile +++ b/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 diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh index d3b7715904..edb0bcdcae 100755 --- a/scripts/create-dev-env.sh +++ b/scripts/create-dev-env.sh @@ -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.