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/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py index fbc9409e7b..dfe3803dfd 100644 --- a/common/djangoapps/terrain/course_helpers.py +++ b/common/djangoapps/terrain/course_helpers.py @@ -10,7 +10,8 @@ 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 @@ -110,7 +111,6 @@ def save_the_course_content(path='/tmp'): u = world.browser.url section_url = u[u.find('courseware/') + 11:] - if not os.path.exists(path): os.makedirs(path) @@ -129,6 +129,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/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