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