Merge pull request #17599 from edx/fsheets/EDUCATOR-2324

Clean up old Studio Assets Page
This commit is contained in:
Farhanah Sheets
2018-03-08 14:06:02 -05:00
committed by GitHub
14 changed files with 29 additions and 1169 deletions

View File

@@ -1,90 +0,0 @@
"""
Tests for the models that control the
persistent grading feature.
"""
import itertools
import ddt
from django.test import TestCase
from opaque_keys.edx.locator import CourseLocator
from contentstore.config.models import NewAssetsPageFlag
from contentstore.config.tests.utils import new_assets_page_feature_flags
@ddt.ddt
class NewAssetsPageFlagTests(TestCase):
"""
Tests the behavior of the feature flags for the new assets page.
These are set via Django admin settings.
"""
def setUp(self):
super(NewAssetsPageFlagTests, self).setUp()
self.course_id_1 = CourseLocator(org="edx", course="course", run="run")
self.course_id_2 = CourseLocator(org="edx", course="course2", run="run")
@ddt.data(*itertools.product(
(True, False),
(True, False),
(True, False),
))
@ddt.unpack
def test_new_assets_page_feature_flags(self, global_flag, enabled_for_all_courses, enabled_for_course_1):
with new_assets_page_feature_flags(
global_flag=global_flag,
enabled_for_all_courses=enabled_for_all_courses,
course_id=self.course_id_1,
enabled_for_course=enabled_for_course_1
):
self.assertEqual(NewAssetsPageFlag.feature_enabled(), global_flag and enabled_for_all_courses)
self.assertEqual(
NewAssetsPageFlag.feature_enabled(self.course_id_1),
global_flag and (enabled_for_all_courses or enabled_for_course_1)
)
self.assertEqual(
NewAssetsPageFlag.feature_enabled(self.course_id_2),
global_flag and enabled_for_all_courses
)
def test_enable_disable_course_flag(self):
"""
Ensures that the flag, once enabled for a course, can also be disabled.
"""
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=True
):
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=False
):
self.assertFalse(NewAssetsPageFlag.feature_enabled(self.course_id_1))
def test_enable_disable_globally(self):
"""
Ensures that the flag, once enabled globally, can also be disabled.
"""
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=True,
):
self.assertTrue(NewAssetsPageFlag.feature_enabled())
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=True
):
self.assertFalse(NewAssetsPageFlag.feature_enabled())
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=False,
):
self.assertFalse(NewAssetsPageFlag.feature_enabled())
self.assertFalse(NewAssetsPageFlag.feature_enabled(self.course_id_1))

View File

@@ -1,27 +0,0 @@
"""
Provides helper functions for tests that want
to configure flags related to persistent grading.
"""
from contextlib import contextmanager
from contentstore.config.models import NewAssetsPageFlag, CourseNewAssetsPageFlag
from openedx.core.djangoapps.request_cache.middleware import RequestCache
@contextmanager
def new_assets_page_feature_flags(
global_flag,
enabled_for_all_courses=False,
course_id=None,
enabled_for_course=False
):
"""
Most test cases will use a single call to this manager,
as they need to set the global setting and the course-specific
setting for a single course.
"""
RequestCache.clear_request_cache()
NewAssetsPageFlag.objects.create(enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses)
if course_id:
CourseNewAssetsPageFlag.objects.create(course_id=course_id, enabled=enabled_for_course)
yield

View File

@@ -1,84 +0,0 @@
@shard_1 @requires_stub_youtube
Feature: CMS Transcripts
As a course author, I want to be able to create video components
# For transcripts acceptance tests there are 3 available caption
# files. They can be used to test various transcripts features. Two of
# them can be imported from YouTube.
#
# The length of each file name is 11 characters. This is because the
# YouTube's ID length is 11 characters. If file name is not of length 11,
# front-end validation will not pass.
#
# t__eq_exist - this file exists on YouTube, and can be imported
# via the transcripts menu; after import, this file will
# be equal to the one stored locally
# t_neq_exist - same as above, except local file will differ from the
# one stored on YouTube
# t_not_exist - this file does not exist on YouTube; it exists locally
#3
Scenario: Youtube id only: check "not found" and "import" states
Given I have created a Video component with subtitles
And I edit the component
# Not found: w/o local or server subs
And I remove "t_not_exist" transcripts id from store
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "not found"
And I see value "" in the field "Default Timed Transcript"
# Import: w/o local but with server subs
And I remove "t__eq_exist" transcripts id from store
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
Then I see status message "not found on edx"
And I see button "import"
And I click transcript button "import"
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I see button "download_to_edit"
And I see value "t__eq_exist" in the field "Default Timed Transcript"
# Disabled 1/29/14 due to flakiness observed in master
#10
#Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o subs
# Given I have created a Video component
# And I edit the component
#
# And I enter a "http://youtu.be/t__eq_exist" source to field number 1
# Then I see status message "not found on edx"
# And I see button "import"
# And I click transcript button "import"
# Then I see status message "found"
#
# And I enter a "t_not_exist.mp4" source to field number 2
# Then I see status message "found"
# And I see value "t__eq_exist" in the field "Default Timed Transcript"
# Flaky test fails occasionally in master. https://openedx.atlassian.net/browse/BLD-892
#21
#Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o #transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
# Given I have created a Video component with subtitles "t_not_exist"
# And I edit the component
#
# And I enter a "t_not_exist.mp4" source to field number 1
# Then I see status message "found"
# And I see button "download_to_edit"
# And I see button "upload_new_timed_transcripts"
# And I see value "t_not_exist" in the field "Default Timed Transcript"
#
# And I save changes
# And I edit the component
#
# And I enter a "video_name_2.mp4" source to field number 1
# Then I see status message "use existing"
# And I see button "use_existing"
# And I click transcript button "use_existing"
# And I see value "video_name_2" in the field "Default Timed Transcript"
#
# And I enter a "video_name_3.mp4" source to field number 1
# Then I see status message "use existing"
# And I see button "use_existing"
# And I click transcript button "use_existing"
# And I see value "video_name_3" in the field "Default Timed Transcript"

View File

@@ -1,261 +0,0 @@
# disable missing docstring
# pylint: disable=missing-docstring
import os
from django.conf import settings
from lettuce import step, world
from splinter.request_handler.request_handler import RequestHandler
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
# We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5
ERROR_MESSAGES = {
'url_format': u'Incorrect url format.',
'file_type': u'Link types should be unique.',
'links_duplication': u'Links should be unique.',
}
STATUSES = {
'found': u'Timed Transcript Found',
'not found on edx': u'No EdX Timed Transcript',
'not found': u'No Timed Transcript',
'replace': u'Timed Transcript Conflict',
'uploaded_successfully': u'Timed Transcript Uploaded Successfully',
'use existing': u'Confirm Timed Transcript',
}
SELECTORS = {
'error_bar': '.transcripts-error-message',
'url_inputs': '.videolist-settings-item input.input',
'collapse_link': '.collapse-action.collapse-setting',
'collapse_bar': '.videolist-extra-videos',
'status_bar': '.transcripts-message-status',
}
# button type , button css selector, button message
TRANSCRIPTS_BUTTONS = {
'import': ('.setting-import', 'Import YouTube Transcript'),
'download_to_edit': ('.setting-download', 'Download Transcript for Editing'),
'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'),
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'),
'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'),
'choose': ('.setting-choose', 'Timed Transcript from {}'),
'use_existing': ('.setting-use-existing', 'Use Current Transcript'),
}
@step('I clear fields$')
def clear_fields(_step):
# Clear the input fields and trigger an 'input' event
script = """
$('{selector}')
.prop('disabled', false)
.removeClass('is-disabled')
.attr('aria-disabled', false)
.val('')
.trigger('input');
""".format(selector=SELECTORS['url_inputs'])
world.browser.execute_script(script)
world.wait(DELAY)
world.wait_for_ajax_complete()
@step('I clear field number (.+)$')
def clear_field(_step, index):
index = int(index) - 1
world.css_fill(SELECTORS['url_inputs'], '', index)
# For some reason ChromeDriver doesn't trigger an 'input' event after filling
# the field with an empty value. That's why we trigger it manually via jQuery.
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
world.wait(DELAY)
world.wait_for_ajax_complete()
@step('I expect (.+) inputs are disabled$')
def inputs_are_disabled(_step, indexes):
index_list = [int(i.strip()) - 1 for i in indexes.split(',')]
for index in index_list:
el = world.css_find(SELECTORS['url_inputs'])[index]
assert el['disabled']
@step('I expect inputs are enabled$')
def inputs_are_enabled(_step):
for index in range(3):
el = world.css_find(SELECTORS['url_inputs'])[index]
assert not el['disabled']
@step('I do not see error message$')
def i_do_not_see_error_message(_step):
assert not world.css_visible(SELECTORS['error_bar'])
@step('I see error message "([^"]*)"$')
def i_see_error_message(_step, error):
assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error])
@step('I do not see status message$')
def i_do_not_see_status_message(_step):
assert not world.css_visible(SELECTORS['status_bar'])
@step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status):
assert not world.css_visible(SELECTORS['error_bar'])
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status])
DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0]
if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) and not world.css_find(DOWNLOAD_BUTTON)[0].has_class('is-disabled'):
assert _transcripts_are_downloaded()
@step('I (.*)see button "([^"]*)"$')
def i_see_button(_step, not_see, button_type):
button = button_type.strip()
if not_see.strip():
assert world.is_css_not_present(TRANSCRIPTS_BUTTONS[button][0])
else:
assert world.css_has_text(TRANSCRIPTS_BUTTONS[button][0], TRANSCRIPTS_BUTTONS[button][1])
@step(r'I (.*)see (.*)button "([^"]*)" number (\d+)$')
def i_see_button_with_custom_text(_step, not_see, button_type, custom_text, index):
button = button_type.strip()
custom_text = custom_text.strip()
index = int(index.strip()) - 1
if not_see.strip():
assert world.is_css_not_present(TRANSCRIPTS_BUTTONS[button][0])
else:
assert world.css_has_text(TRANSCRIPTS_BUTTONS[button][0], TRANSCRIPTS_BUTTONS[button][1].format(custom_text), index)
@step('I click transcript button "([^"]*)"$')
def click_button_transcripts_variant(_step, button_type):
button = button_type.strip()
world.css_click(TRANSCRIPTS_BUTTONS[button][0])
world.wait_for_ajax_complete()
@step(r'I click transcript button "([^"]*)" number (\d+)$')
def click_button_index(_step, button_type, index):
button = button_type.strip()
index = int(index.strip()) - 1
world.css_click(TRANSCRIPTS_BUTTONS[button][0], index)
world.wait_for_ajax_complete()
@step('I remove "([^"]+)" transcripts id from store')
def remove_transcripts_from_store(_step, subs_id):
"""Remove from store, if transcripts content exists."""
filename = 'subs_{0}.srt.sjson'.format(subs_id.strip())
content_location = StaticContent.compute_location(
world.scenario_dict['COURSE'].id,
filename
)
try:
content = contentstore().find(content_location)
contentstore().delete(content.location)
print 'Transcript file was removed from store.'
except NotFoundError:
print 'Transcript file was NOT found and not removed.'
@step(r'I enter a "([^"]+)" source to field number (\d+)$')
def i_enter_a_source(_step, link, index):
index = int(index) - 1
if index is not 0 and not world.css_visible(SELECTORS['collapse_bar']):
world.css_click(SELECTORS['collapse_link'])
assert world.css_visible(SELECTORS['collapse_bar'])
world.css_fill(SELECTORS['url_inputs'], link, index)
world.wait(DELAY)
world.wait_for_ajax_complete()
@step('I upload the transcripts file "([^"]*)"$')
def upload_file(_step, file_name):
path = os.path.join(TEST_ROOT, 'uploads/', file_name.strip())
world.browser.execute_script("$('form.file-chooser').show()")
world.browser.attach_file('transcript-file', os.path.abspath(path))
world.wait_for_ajax_complete()
@step('I see "([^"]*)" text in the captions')
def check_text_in_the_captions(_step, text):
world.wait_for_present('.video.is-captions-rendered')
world.wait_for(lambda _: world.css_text('.subtitles'), timeout=30)
actual_text = world.css_text('.subtitles')
assert text in actual_text
@step('I see value "([^"]*)" in the field "([^"]*)"$')
def check_transcripts_field(_step, values, field_name):
world.select_editor_tab('Advanced')
tab = world.css_find('#settings-tab').first
field_id = '#' + tab.find_by_xpath('.//label[text()="%s"]' % field_name.strip())[0]['for']
values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')]
assert any(values_list)
world.select_editor_tab('Basic')
@step('I save changes$')
def save_changes(_step):
world.save_component()
@step('I open tab "([^"]*)"$')
def open_tab(_step, tab_name):
world.select_editor_tab(tab_name)
@step('I set value "([^"]*)" to the field "([^"]*)"$')
def set_value_transcripts_field(_step, value, field_name):
tab = world.css_find('#settings-tab').first
XPATH = './/label[text()="{name}"]'.format(name=field_name)
SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for']
element = world.css_find(SELECTOR).first
if element['type'] == 'text':
SCRIPT = '$("{selector}").val("{value}").change()'.format(
selector=SELECTOR,
value=value
)
world.browser.execute_script(SCRIPT)
assert world.css_has_value(SELECTOR, value)
else:
assert False, 'Incorrect element type.'
world.wait_for_ajax_complete()
@step('I revert the transcript field "([^"]*)"$')
def revert_transcripts_field(_step, field_name):
world.revert_setting_entry(field_name)
def _transcripts_are_downloaded():
world.wait_for_ajax_complete()
request = RequestHandler()
DOWNLOAD_BUTTON = world.css_find(TRANSCRIPTS_BUTTONS["download_to_edit"][0]).first
url = DOWNLOAD_BUTTON['href']
request.connect(url)
return request.status_code.is_success()

View File

@@ -1,118 +0,0 @@
@shard_2
Feature: CMS.Upload Files
As a course author, I want to be able to upload files for my students
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can upload files
Given I am at the files and upload page of a Studio course
When I upload the file "test" by clicking "Upload your first asset"
Then I should see the file "test" was uploaded
And The url for the file "test" is valid
When I upload the file "test2"
Then I should see the file "test2" was uploaded
And The url for the file "test2" is valid
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can upload multiple files
Given I am at the files and upload page of a Studio course
When I upload the files "test,test2"
Then I should see the file "test" was uploaded
And I should see the file "test2" was uploaded
And The url for the file "test2" is valid
And The url for the file "test" is valid
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can update files
Given I am at the files and upload page of a Studio course
When I upload the file "test"
And I upload the file "test"
Then I should see only one "test"
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can delete uploaded files
Given I am at the files and upload page of a Studio course
When I upload the file "test"
And I delete the file "test"
Then I should not see the file "test" was uploaded
And I see a confirmation that the file was deleted
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can download files
Given I am at the files and upload page of a Studio course
When I upload the file "test"
Then I can download the correct "test" file
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can download updated files
Given I am at the files and upload page of a Studio course
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
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can lock assets through asset index
Given I am at the files and upload page of a Studio course
When I upload an asset
And I lock the asset
Then the asset is locked
And I see a "saving" notification
And I reload the page
Then the asset is locked
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can unlock assets through asset index
Given I have created a course with a locked asset
When I unlock the asset
Then the asset is unlocked
And I see a "saving" notification
And I reload the page
Then the asset is unlocked
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Locked assets can't be viewed if logged in as an unregistered user
Given I have created a course with a locked asset
And the user "bob" exists
When "bob" logs in
Then the asset is protected
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Locked assets can be viewed if logged in as a registered user
Given I have created a course with a locked asset
And the user "bob" exists
And the user "bob" is enrolled in the course
When "bob" logs in
Then the asset is viewable
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Locked assets can't be viewed if logged out
Given I have created a course with a locked asset
When I log out
Then the asset is protected
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Locked assets can be viewed with is_staff account
Given I have created a course with a locked asset
And the user "staff" exists as a course is_staff
When "staff" logs in
Then the asset is viewable
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Unlocked assets can be viewed by anyone
Given I have created a course with a unlocked asset
When I log out
Then the asset is viewable

View File

@@ -1,230 +0,0 @@
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
import os
import random
import string
import requests
from django.conf import settings
from django.contrib.auth.models import User
from lettuce import step, world
from lettuce.django import django_url
from nose.tools import assert_equal, assert_not_equal
from student.models import CourseEnrollment
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
ASSET_NAMES_CSS = 'td.name-col > span.title > a.filename'
@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 a'
world.css_click(menu_css)
world.css_click(uploads_css)
@step(u'I upload the( test)? file "([^"]*)"$')
def upload_file(_step, is_test_file, file_name, button_text=None):
if button_text:
world.click_link(button_text)
else:
world.click_link('Upload New File')
if not is_test_file:
_write_test_file(file_name, "test file")
# uploading the file itself
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path))
close_css = 'a.close-button'
world.css_click(close_css)
@step(u'I upload the file "([^"]*)" by clicking "([^"]*)"')
def upload_file_on_button_press(_step, file_name, button_text=None):
upload_file(_step, '', file_name, button_text)
@step(u'I upload the files "([^"]*)"$')
def upload_files(_step, files_string):
# files_string should be comma separated with no spaces.
files = files_string.split(",")
upload_css = 'a.upload-button'
world.css_click(upload_css)
# uploading the files
for filename in files:
_write_test_file(filename, "test file")
path = os.path.join(TEST_ROOT, 'uploads/', filename)
world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path))
close_css = 'a.close-button'
world.css_click(close_css)
@step(u'I should not see the file "([^"]*)" was uploaded$')
def check_not_there(_step, file_name):
# Either there are no files, or there are files but
# not the one I expect not to exist.
# Since our only test for deletion right now deletes
# the only file that was uploaded, our success criteria
# will be that there are no files.
# In the future we can refactor if necessary.
assert world.is_css_not_present(ASSET_NAMES_CSS)
@step(u'I should see the file "([^"]*)" was uploaded$')
def check_upload(_step, file_name):
index = get_index(file_name)
assert_not_equal(index, -1)
@step(u'The url for the file "([^"]*)" is valid$')
def check_url(_step, file_name):
r = get_file(file_name)
assert_equal(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)
world.confirm_studio_prompt()
@step(u'I should see only one "([^"]*)"$')
def no_duplicate(_step, file_name):
all_names = world.css_find(ASSET_NAMES_CSS)
only_one = False
for i in range(len(all_names)):
if file_name == world.css_html(ASSET_NAMES_CSS, index=i):
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
# resetting the file back to its original state
_write_test_file(file_name, "This is an arbitrary file for testing uploads")
def _write_test_file(file_name, text):
path = os.path.join(TEST_ROOT, 'uploads/', file_name)
# resetting the file back to its original state
with open(os.path.abspath(path), 'w') as cur_file:
cur_file.write(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))
_write_test_file(file_name, new_text)
@step(u'I upload an asset$')
def upload_an_asset(step):
step.given('I upload the file "asset.html"')
@step(u'I (lock|unlock) the asset$')
def lock_unlock_file(_step, _lock_state):
index = get_index('asset.html')
assert index != -1, 'Expected to find an asset but could not.'
# Warning: this is a misnomer, it really only toggles the
# lock state. TODO: fix it.
lock_css = "input.lock-checkbox"
world.css_find(lock_css)[index].click()
@step(u'the user "([^"]*)" is enrolled in the course$')
def user_foo_is_enrolled_in_the_course(step, name):
world.create_user(name, 'test')
user = User.objects.get(username=name)
course_id = world.scenario_dict['COURSE'].id
CourseEnrollment.enroll(user, course_id)
@step(u'Then the asset is (locked|unlocked)$')
def verify_lock_unlock_file(_step, lock_state):
index = get_index('asset.html')
assert index != -1, 'Expected to find an asset but could not.'
lock_css = "input.lock-checkbox"
checked = world.css_find(lock_css)[index]._element.get_attribute('checked')
assert_equal(lock_state == "locked", bool(checked))
@step(u'I am at the files and upload page of a Studio course')
def at_upload_page(step):
step.given('I have opened a new course in studio')
step.given('I go to the files and uploads page')
@step(u'I have created a course with a (locked|unlocked) asset$')
def open_course_with_locked(step, lock_state):
step.given('I am at the files and upload page of a Studio course')
step.given('I upload the file "asset.html"')
if lock_state == "locked":
step.given('I lock the asset')
step.given('I reload the page')
@step(u'Then the asset is (viewable|protected)$')
def view_asset(_step, status):
asset_loc = world.scenario_dict['COURSE'].id.make_asset_key(asset_type='asset', path='asset.html')
svr_loc = django_url()
asset_url = unicode(asset_loc)
divider = '/'
if asset_url[0] == '/':
divider = ''
url = '{}{}{}'.format(svr_loc, divider, asset_url)
if status == 'viewable':
expected_text = 'test file'
else:
expected_text = 'Unauthorized'
# Note that world.visit would trigger a 403 error instead of displaying "Unauthorized"
# Instead, we can drop back into the selenium driver get command.
world.browser.driver.get(url)
assert_equal(world.css_text('body'), expected_text)
@step('I see a confirmation that the file was deleted$')
def i_see_a_delete_confirmation(_step):
alert_css = '#notification-confirmation'
assert world.is_css_present(alert_css)
def get_index(file_name):
all_names = world.css_find(ASSET_NAMES_CSS)
for i in range(len(all_names)):
if file_name == world.css_html(ASSET_NAMES_CSS, index=i):
return i
return -1
def get_file(file_name):
index = get_index(file_name)
assert index != -1
url_css = 'a.filename'
def get_url():
return world.css_find(url_css)[index]._element.get_attribute('href')
url = world.retry_on_exception(get_url)
return requests.get(url)

View File

@@ -1,67 +0,0 @@
# pylint: disable=missing-docstring
from lettuce import step, world
SELECTORS = {
'spinner': '.video-wrapper .spinner',
'controls': '.video-controls',
}
# We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5
@step('I have uploaded subtitles "([^"]*)"$')
def i_have_uploaded_subtitles(_step, sub_id):
_step.given('I go to the files and uploads page')
_step.given('I upload the test file "subs_{}.srt.sjson"'.format(sub_id.strip()))
@step('I have created a Video component$')
def i_created_a_video_component(step):
step.given('I am in Studio editing a new unit')
world.create_component_instance(
step=step,
category='video',
)
world.wait_for_xmodule()
world.disable_jquery_animations()
world.wait_for_present('.is-initialized')
world.wait(DELAY)
world.wait_for_invisible(SELECTORS['spinner'])
if not world.youtube.config.get('youtube_api_blocked'):
world.wait_for_visible(SELECTORS['controls'])
@step('I have created a Video component with subtitles$')
def i_created_a_video_with_subs(_step):
_step.given('I have created a Video component with subtitles "3_yD_cEKoCk"')
@step('I have created a Video component with subtitles "([^"]*)"$')
def i_created_a_video_with_subs_with_name(_step, sub_id):
_step.given('I have created a Video component')
# Store the current URL so we can return here
video_url = world.browser.url
# Upload subtitles for the video using the upload interface
_step.given('I have uploaded subtitles "{}"'.format(sub_id))
# Return to the video
world.visit(video_url)
world.wait_for_xmodule()
# update .sub filed with proper subs name (which mimics real Studio/XML behavior)
# this is needed only for that videos which are created in acceptance tests.
_step.given('I edit the component')
world.wait_for_ajax_complete()
_step.given('I save changes')
world.disable_jquery_animations()
world.wait_for_present('.is-initialized')
world.wait_for_invisible(SELECTORS['spinner'])

View File

@@ -20,7 +20,6 @@ from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.config.models import NewAssetsPageFlag
from contentstore.utils import reverse_course_url
from contentstore.views.exception import AssetNotFoundException, AssetSizeTooLargeException
from edxmako.shortcuts import render_to_response
@@ -102,7 +101,6 @@ def _asset_index(request, course_key):
return render_to_response('asset_index.html', {
'language_code': request.LANGUAGE_CODE,
'waffle_flag_enabled': NewAssetsPageFlag.feature_enabled(course_key),
'context_course': course_module,
'max_file_size_in_mbs': settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
'chunk_size_in_mbs': settings.UPLOAD_CHUNK_SIZE_IN_MB,

View File

@@ -14,31 +14,8 @@
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% if waffle_flag_enabled:
% if not settings.STUDIO_FRONTEND_CONTAINER_URL:
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/vendor.min.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/assets.min.css')}" />
% endif
% else:
% for template_name in ["asset"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
% endif
</%block>
<%block name="requirejs">
% if not waffle_flag_enabled:
require(["js/factories/asset_index"], function (AssetIndexFactory) {
AssetIndexFactory({
assetCallbackUrl: "${asset_callback_url|n, js_escaped_string}",
uploadChunkSizeInMBs: ${chunk_size_in_mbs|n, dump_js_escaped_json},
maxFileSizeInMBs: ${max_file_size_in_mbs|n, dump_js_escaped_json},
maxFileSizeRedirectUrl: "${max_file_size_redirect_url|n, js_escaped_string}"
});
});
% endif
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/vendor.min.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/assets.min.css')}" />
</%block>
<%block name="content">
@@ -49,122 +26,36 @@
<small class="subtitle">${_("Content")}</small>
<span class="sr">- </span>${_("Files & Uploads")}
</h3>
% if not waffle_flag_enabled:
<div class="nav-actions">
<ul>
<li class="nav-item">
<a href="#" class="button upload-button new-button"><span class="icon fa fa-plus" aria-hidden="true"></span> ${_("Upload New File")}</a>
</li>
</ul>
</div>
% endif
</header>
</div>
<div class="wrapper-content wrapper">
<div class="content">
<%static:optional_include_mako file="asset_index_content_header.html" />
% if waffle_flag_enabled:
<%static:studiofrontend entry="assets">
{
"lang": "${language_code | n, js_escaped_string}",
"course": {
"id": "${context_course.id | n, js_escaped_string}",
"name": "${context_course.display_name_with_default | n, js_escaped_string}",
"url_name": "${context_course.location.name | n, js_escaped_string}",
"org": "${context_course.location.org | n, js_escaped_string}",
"num": "${context_course.location.course | n, js_escaped_string}",
"display_course_number": "${context_course.display_coursenumber | n, js_escaped_string}",
"revision": "${context_course.location.revision | n, js_escaped_string}"
},
"help_tokens": {
"files": "${get_online_help_info(online_help_token())['doc_url'] | n, js_escaped_string}"
},
"upload_settings": {
"max_file_size_in_mbs": ${max_file_size_in_mbs|n, dump_js_escaped_json}
},
"search_settings": {
"enabled": ${waffle().is_enabled(ENABLE_ASSETS_SEARCH) | n, dump_js_escaped_json}
}
<%static:studiofrontend entry="assets">
{
"lang": "${language_code | n, js_escaped_string}",
"course": {
"id": "${context_course.id | n, js_escaped_string}",
"name": "${context_course.display_name_with_default | n, js_escaped_string}",
"url_name": "${context_course.location.name | n, js_escaped_string}",
"org": "${context_course.location.org | n, js_escaped_string}",
"num": "${context_course.location.course | n, js_escaped_string}",
"display_course_number": "${context_course.display_coursenumber | n, js_escaped_string}",
"revision": "${context_course.location.revision | n, js_escaped_string}"
},
"help_tokens": {
"files": "${get_online_help_info(online_help_token())['doc_url'] | n, js_escaped_string}"
},
"upload_settings": {
"max_file_size_in_mbs": ${max_file_size_in_mbs|n, dump_js_escaped_json}
},
"search_settings": {
"enabled": ${waffle().is_enabled(ENABLE_ASSETS_SEARCH) | n, dump_js_escaped_json}
}
</%static:studiofrontend>
% else:
<div class="content-primary">
<div class="wrapper-assets"></div>
<div class="ui-loading">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
</div>
</div>
% endif
% if not waffle_flag_enabled:
<aside class="content-supplementary" role="complementary" aria-label="${_("Help adding Files and Uploads")}">
<div class="bit">
<h3 class="title-3">${_("Adding Files for Your Course")}</h3>
<p>${Text(_("To add files to use in your course, click {em_start}Upload New File{em_end}. Then follow the prompts to upload a file from your computer.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
<p>${Text(_("{em_start}Caution{em_end}: {platform_name} recommends that you limit the file size to {em_start}10 MB{em_end}. In addition, do not upload video or audio files. You should use a third party service to host multimedia files.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"), platform_name=settings.PLATFORM_NAME)}</p>
<p>${_("The course image, textbook chapters, and files that appear on your Course Handouts sidebar also appear in this list.")}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("Using File URLs")}</h3>
<p>${Text(_("Use the {em_start}{studio_name} URL{em_end} value to link to the file or image from a component, a course update, or a course handout.")).format(studio_name=settings.STUDIO_SHORT_NAME, em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
<p>${Text(_("Use the {em_start}Web URL{em_end} value to reference the file or image only from outside of your course. {em_start}Note:{em_end} If you lock a file, the Web URL no longer works for external access to a file.")).format(em_start=HTML("<strong>"), em_end=HTML("</strong>"))}</p>
<p>${_("To copy a URL, double click the value in the URL column, then copy the selected text.")}</p>
</div>
<div class="bit external-help">
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about managing files")}</a>
</div>
</aside>
% endif
}
</%static:studiofrontend>
</div>
</div>
<div class="upload-modal modal">
<a href="#" class="close-button"><span class="icon fa fa-times-circle" aria-hidden="true"></span> <span class="sr">${_('close')}</span></a>
<div class="modal-body">
<h2 class="title">${_("Upload New File")}</h2>
<p>${_("Max per-file size: {max_filesize}MB").format(max_filesize=max_file_size_in_mbs)}</p>
<p class="file-name">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<div class="embeddable">
<label>${_('URL:')}</label>
<input type="text" class="embeddable-xml-input" value='' readonly>
</div>
<form class="file-chooser" action="${asset_callback_url}"
method="post" enctype="multipart/form-data">
<a href="#" class="choose-file-button">${_("Choose File")}</a>
<input type="file" class="file-input" name="file" multiple>
</form>
</div>
</div>
</%block>
<%block name="view_alerts">
<!-- alert: save confirmed with close -->
<div class="wrapper wrapper-alert wrapper-alert-confirmation" role="status">
<div class="alert confirmation">
<span class="icon fa fa-check" aria-hidden="true"></span>
<div class="copy">
<h2 class="title title-3">${_('Your file has been deleted.')}</h2>
</div>
<a href="" rel="view" class="action action-alert-close">
<span class="icon fa fa-times-circle" aria-hidden="true"></span>
<span class="label">${_('close alert')}</span>
</a>
</div>
</div>
</%block>

View File

@@ -5,14 +5,12 @@ from django.contrib.admin import autodiscover as django_autodiscover
from django.utils.translation import ugettext_lazy as _
import contentstore.views
from contentstore.config.models import NewAssetsPageFlag
from cms.djangoapps.contentstore.views.organization import OrganizationListView
import openedx.core.djangoapps.common_views.xblock
import openedx.core.djangoapps.debug.views
import openedx.core.djangoapps.external_auth.views
import openedx.core.djangoapps.lang_pref.views
from config_models.views import ConfigurationModelCurrentAPIView
from ratelimitbackend import admin
django_autodiscover()
@@ -250,10 +248,6 @@ if 'debug_toolbar' in settings.INSTALLED_APPS:
urlpatterns.append(url(r'^template/(?P<template>.+)$', openedx.core.djangoapps.debug.views.show_reference_template,
name='openedx.core.djangoapps.debug.views.show_reference_template'))
urlpatterns += [
url(r'config/assets', ConfigurationModelCurrentAPIView.as_view(model=NewAssetsPageFlag)),
]
# Custom error pages
# These are used by Django to render these error codes. Do not remove.
# pylint: disable=invalid-name

View File

@@ -18,83 +18,6 @@ UPLOAD_SUFFIX = '/data/uploads/studio-uploads/'
UPLOAD_FILE_DIR = Path(__file__).abspath().dirname().dirname().dirname().dirname() + UPLOAD_SUFFIX # pylint: disable=no-value-for-parameter
class AssetIndexPage(CoursePage):
"""
The Files and Uploads page for a course in Studio
"""
url_path = "assets"
type_filter_element = '#js-asset-type-col'
@property
def url(self):
"""
Construct a URL to the page within the course.
"""
# TODO - is there a better way to make this agnostic to the underlying default module store?
default_store = os.environ.get('DEFAULT_STORE', 'draft')
course_key = CourseLocator(
self.course_info['course_org'],
self.course_info['course_num'],
self.course_info['course_run'],
deprecated=(default_store == 'draft')
)
url = "/".join([BASE_URL, self.url_path, urllib.quote_plus(unicode(course_key))])
return url if url[-1] == '/' else url + '/'
@wait_for_js
def is_browser_on_page(self):
return all([
self.q(css='body.view-uploads').present,
self.q(css='.page-header').present,
not self.q(css='div.ui-loading').visible,
])
@wait_for_js
def type_filter_on_page(self):
"""
Checks that type filter is in table header.
"""
return self.q(css=self.type_filter_element).present
@wait_for_js
def type_filter_header_label_visible(self):
"""
Checks type filter label is added and visible in the pagination header.
"""
return self.q(css='span.filter-column').visible
@wait_for_js
def click_type_filter(self):
"""
Clicks type filter menu.
"""
self.q(css=".filterable-column .nav-item").click()
@wait_for_js
def select_type_filter(self, filter_number):
"""
Selects Type filter from dropdown which filters the results.
Returns False if no filter.
"""
self.wait_for_ajax()
if self.q(css=".filterable-column .nav-item").is_present():
if not self.q(css=self.type_filter_element + " .wrapper-nav-sub").visible:
self.q(css=".filterable-column > .nav-item").first.click()
self.wait_for_element_visibility(
self.type_filter_element + " .wrapper-nav-sub", "Type Filter promise satisfied.")
self.q(css=self.type_filter_element + " .column-filter-link").nth(filter_number).click()
self.wait_for_ajax()
return True
return False
def return_results_set(self):
"""
Returns the asset set from the page
"""
return self.q(css="#asset-table-body tr").results
class AssetIndexPageStudioFrontend(CoursePage):
"""The Files and Uploads page for a course in Studio"""

View File

@@ -3,59 +3,11 @@ Acceptance tests for Studio related to the asset index page.
"""
import os
from common.test.acceptance.fixtures.base import StudioApiLoginError
from common.test.acceptance.fixtures.config import ConfigModelFixture
from common.test.acceptance.pages.studio.asset_index import AssetIndexPage, AssetIndexPageStudioFrontend
from common.test.acceptance.tests.helpers import skip_if_browser
from common.test.acceptance.pages.studio.asset_index import AssetIndexPageStudioFrontend
from common.test.acceptance.tests.studio.base_studio_test import StudioCourseTest
from common.test.acceptance.pages.studio.asset_index import UPLOAD_FILE_DIR
class AssetIndexTest(StudioCourseTest):
"""
Tests for the Asset index page.
"""
def setUp(self, is_staff=False): # pylint: disable=arguments-differ
super(AssetIndexTest, self).setUp()
self.asset_page = AssetIndexPage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
def populate_course_fixture(self, course_fixture):
"""
Populate the children of the test course fixture.
"""
ConfigModelFixture('/config/assets', {'enabled_for_all_courses': False, 'enabled': False}, 'cms').install()
self.course_fixture.add_asset(['image.jpg', 'textbook.pdf'])
@skip_if_browser('chrome') # TODO Need to fix test_page_existance for this for chrome browser
def test_type_filter_exists(self):
"""
Make sure type filter is on the page.
"""
self.asset_page.visit()
assert self.asset_page.type_filter_on_page() is True
@skip_if_browser('chrome') # TODO Need to fix test_page_existance for this for chrome browser
def test_filter_results(self):
"""
Make sure type filter actually filters the results.
"""
self.asset_page.visit()
all_results = len(self.asset_page.return_results_set())
if self.asset_page.select_type_filter(1):
filtered_results = len(self.asset_page.return_results_set())
assert self.asset_page.type_filter_header_label_visible()
assert all_results > filtered_results
else:
msg = "Could not open select Type filter"
raise StudioApiLoginError(msg)
class AssetIndexTestStudioFrontend(StudioCourseTest):
"""Tests for the Asset index page."""
@@ -70,7 +22,6 @@ class AssetIndexTestStudioFrontend(StudioCourseTest):
def populate_course_fixture(self, course_fixture):
"""Populate the children of the test course fixture."""
ConfigModelFixture('/config/assets', {'enabled_for_all_courses': True, 'enabled': True}, 'cms').install()
self.course_fixture.add_asset(['image.jpg', 'textbook.pdf'])
def test_page_with_assets_elements_load(self):
@@ -233,7 +184,6 @@ class AssetIndexTestStudioFrontendPagination(StudioCourseTest):
def populate_course_fixture(self, course_fixture):
"""Populate the children of the test course fixture and upload 49 files."""
ConfigModelFixture('/config/assets', {'enabled_for_all_courses': True, 'enabled': True}, 'cms').install()
files = []
for file_name in os.listdir(UPLOAD_FILE_DIR):

View File

@@ -6,7 +6,7 @@ import uuid
from base_studio_test import StudioCourseTest
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.asset_index import AssetIndexPage
from common.test.acceptance.pages.studio.asset_index import AssetIndexPageStudioFrontend
from common.test.acceptance.pages.studio.course_info import CourseUpdatesPage
from common.test.acceptance.pages.studio.edit_tabs import PagesPage
from common.test.acceptance.pages.studio.import_export import ExportCoursePage, ImportCoursePage
@@ -227,7 +227,7 @@ class CoursePagesTest(StudioCourseTest):
self.pages = [
clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'])
for clz in [
AssetIndexPage,
AssetIndexPageStudioFrontend,
CourseUpdatesPage,
PagesPage, ExportCoursePage, ImportCoursePage, CourseTeamPage, CourseOutlinePage, SettingsPage,
AdvancedSettingsPage, GradingPage, TextbookUploadPage

View File

@@ -8,7 +8,7 @@ from nose.plugins.attrib import attr
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.asset_index import AssetIndexPage
from common.test.acceptance.pages.studio.asset_index import AssetIndexPageStudioFrontend
from common.test.acceptance.pages.studio.course_info import CourseUpdatesPage
from common.test.acceptance.pages.studio.edit_tabs import PagesPage
from common.test.acceptance.pages.studio.import_export import (
@@ -600,7 +600,7 @@ class AssetIndexHelpTest(StudioCourseTest):
"""
def setUp(self): # pylint: disable=arguments-differ
super(AssetIndexHelpTest, self).setUp()
self.course_asset_index_page = AssetIndexPage(
self.course_asset_index_page = AssetIndexPageStudioFrontend(
self.browser,
self.course_info['org'],
self.course_info['number'],
@@ -626,25 +626,6 @@ class AssetIndexHelpTest(StudioCourseTest):
href=expected_url,
)
def test_asset_index_side_bar_help(self):
"""
Scenario: Help link in sidebar links is working on 'Files & Uploads' page
Given that I am on the 'Files & Uploads' page.
And I want help about the process
And I click the 'Learn more about managing files' in the sidebar links
Then Help link should open.
And help url should be correct
"""
expected_url = _get_expected_documentation_url('/course_assets/course_files.html')
# Assert that help link is correct.
assert_side_bar_help_link(
test=self,
page=self.course_asset_index_page,
href=expected_url,
help_text='Learn more about managing files'
)
@attr(shard=10)
class CoursePagesHelpTest(StudioCourseTest):