Commit 1cbc1716 by Ned Batchelder

Merge master in prep for merging back the hotfix changes.

parents be3ab1c5 09befd2a
......@@ -17,6 +17,7 @@ cms/envs/private.py
/nbproject
.idea/
.redcar/
codekit-config.json
### OS X artifacts
*.DS_Store
......@@ -48,14 +49,18 @@ reports/
.prereqs_cache
.vagrant/
node_modules
.bundle/
bin/
### Static assets pipeline artifacts
*.scssc
lms/static/css/
lms/static/sass/*.css
lms/static/sass/application.scss
lms/static/sass/application-extend1.scss
lms/static/sass/application-extend2.scss
lms/static/sass/course.scss
cms/static/css/
cms/static/sass/*.css
### Logging artifacts
......
......@@ -97,3 +97,5 @@ Iain Dunning <idunning@mit.edu>
Olivier Marquez <oliviermarquez@gmail.com>
Florian Dufour <neurolit@gmail.com>
Manuel Freire <manuel.freire@fdi.ucm.es>
Daniel Cebrián Robles <danielcebrianr@gmail.com>
Carson Gee <cgee@mit.edu>
......@@ -5,6 +5,21 @@ 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: Video Transcripts: Fix clear and download buttons. BLD-438.
Common: Switch over from MITX_FEATURES to just FEATURES. To override items in
the FEATURES dict, the environment variable you must set to do so is also
now called FEATURES instead of MITX_FEATURES.
LMS: Change the forum role granted to global staff on enrollment in a
course. Previously, staff were given the Moderator role; now, they are
given the Student role.
Blades: Fix Numerical input to support mathematical operations. BLD-525.
Blades: Improve calculator's tooltip accessibility. Add possibility to navigate
through the hints via arrow keys. BLD-533.
LMS: Add feature for providing background grade report generation via Celery
instructor task, with reports uploaded to S3. Feature is visible on the beta
instructor dashboard. LMS-58
......@@ -13,9 +28,38 @@ Blades: Added grading support for LTI module. LTI providers can now grade
student's work and send edX scores. OAuth1 based authentication
implemented. BLD-384.
LMS: Beta-tester status is now set on a per-course-run basis, rather than being valid
across all runs with the same course name. Old group membership will still work
across runs, but new beta-testers will only be added to a single course run.
LMS: Beta-tester status is now set on a per-course-run basis, rather than being
valid across all runs with the same course name. Old group membership will
still work across runs, but new beta-testers will only be added to a single
course run.
Blades: Enabled several Video Jasmine tests. BLD-463.
Studio: Continued modification of Studio pages to follow a RESTful framework.
includes Settings pages, edit page for Subsection and Unit, and interfaces
for updating xblocks (xmodules) and getting their editing HTML.
LMS: Improve accessibility of inline discussions in courseware.
Blades: Put 2nd "Hide output" button at top of test box & increase text size for
code response questions. BLD-126.
Blades: Update the calculator hints tooltip with full information. BLD-400.
Blades: Fix transcripts 500 error in studio (BLD-530)
LMS: Add error recovery when a user loads or switches pages in an
inline discussion.
Blades: Allow multiple strings as the correct answer to a string response
question. BLD-474.
Blades: a11y - Videos will alert screenreaders when the video is over.
LMS: Trap focus on the loading element when a user loads more threads
in the forum sidebar to improve accessibility.
LMS: Add error recovery when a user loads more threads in the forum sidebar.
LMS: Add a user-visible alert modal when a forums AJAX request fails.
......@@ -36,7 +80,8 @@ text like with bold or italics. (BLD-449)
LMS: Beta instructor dashboard will only count actively enrolled students for
course enrollment numbers.
Blades: Fix speed menu that is not rendered correctly when YouTube is unavailable. (BLD-457).
Blades: Fix speed menu that is not rendered correctly when YouTube is
unavailable. (BLD-457).
LMS: Users with is_staff=True no longer have the STAFF label appear on
their forum posts.
......@@ -54,6 +99,9 @@ key in course settings. (BLD-426)
Blades: Fix bug when the speed can only be changed when the video is playing.
LMS: The dialogs on the wiki "changes" page are now accessible to screen
readers. Now all wiki pages have been made accessible. (LMS-1337)
LMS: Change bulk email implementation to use less memory, and to better handle
duplicate tasks in celery.
......@@ -70,8 +118,8 @@ client error are correctly passed through to the client.
LMS: Improve performance of page load and thread list load for
discussion tab
LMS: The wiki markup cheatsheet dialog is now accessible to people with
disabilites. (LMS-1303)
LMS: The wiki markup cheatsheet dialog is now accessible to screen readers.
(LMS-1303)
Common: Add skip links for accessibility to CMS and LMS. (LMS-1311)
......
......@@ -33,7 +33,7 @@ class CreatorGroupTest(TestCase):
def test_creator_group_enabled_but_empty(self):
""" Tests creator group feature on, but group empty. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
self.assertFalse(is_user_in_creator_group(self.user))
# Make user staff. This will cause is_user_in_creator_group to return True.
......@@ -42,7 +42,7 @@ class CreatorGroupTest(TestCase):
def test_creator_group_enabled_nonempty(self):
""" Tests creator group feature on, user added. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
self.assertTrue(add_user_to_creator_group(self.admin, self.user))
self.assertTrue(is_user_in_creator_group(self.user))
......@@ -70,7 +70,7 @@ class CreatorGroupTest(TestCase):
def test_course_creation_disabled(self):
""" Tests that the COURSE_CREATION_DISABLED flag overrides course creator group settings. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES',
with mock.patch.dict('django.conf.settings.FEATURES',
{'DISABLE_COURSE_CREATION': True, "ENABLE_CREATOR_GROUP": True}):
# Add user to creator group.
self.assertTrue(add_user_to_creator_group(self.admin, self.user))
......
......@@ -6,14 +6,6 @@ from nose.tools import assert_equal, assert_in # pylint: disable=E0611
from terrain.steps import reload_the_page
def _is_expected_element_count(css, expected_number):
"""
Returns whether the number of elements found on the page by css locator
the same number that you expected.
"""
return len(world.css_find(css)) == expected_number
@world.absorb
def create_component_instance(step, category, component_type=None, is_advanced=False):
"""
......@@ -47,8 +39,11 @@ def create_component_instance(step, category, component_type=None, is_advanced=F
world.wait_for_invisible(component_button_css)
click_component_from_menu(category, component_type, is_advanced)
world.wait_for(lambda _: _is_expected_element_count(module_css,
module_count_before + 1))
expected_count = module_count_before + 1
world.wait_for(
lambda _: len(world.css_find(module_css)) == expected_count,
timeout=20
)
@world.absorb
......
......@@ -76,3 +76,17 @@ Feature: CMS.Course updates
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
And when I reload the page
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
Scenario: Users cannot save handouts with bad html until edit or update it properly
Given I have opened a new course in Studio
And I go to the course updates page
When I modify the handout to "<p><a href=>[LINK TEXT]</a></p>"
Then I see the handout error text
And I see handout save button disabled
When I edit the handout to "<p><a href='https://www.google.com.pk/'>home</a></p>"
Then I see handout save button re-enabled
When I save handout edit
# Can only do partial text matches because of the quotes with in quotes (and regexp step matching).
Then I see the handout "https://www.google.com.pk/"
And when I reload the page
Then I see the handout "https://www.google.com.pk/"
......@@ -90,6 +90,35 @@ def check_handout(_step, handout):
assert handout in world.css_html(handout_css)
@step(u'I see the handout error text')
def check_handout_error(_step):
handout_error_css = 'div#handout_error'
assert world.css_has_class(handout_error_css, 'is-shown')
@step(u'I see handout save button disabled')
def check_handout_error(_step):
handout_save_button = 'form.edit-handouts-form a.save-button'
assert world.css_has_class(handout_save_button, 'is-disabled')
@step(u'I edit the handout to "([^"]*)"$')
def edit_handouts(_step, text):
type_in_codemirror(0, text)
@step(u'I see handout save button re-enabled')
def check_handout_error(_step):
handout_save_button = 'form.edit-handouts-form a.save-button'
assert not world.css_has_class(handout_save_button, 'is-disabled')
@step(u'I save handout edit')
def check_handout_error(_step):
save_css = 'a.save-button'
world.css_click(save_css)
def change_text(text):
type_in_codemirror(0, text)
save_css = 'a.save-button'
......
......@@ -9,10 +9,8 @@ Feature: CMS.Static Pages
Then I should see a static page named "Empty"
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
And I "delete" the static page
Given I have created a static page
When I "delete" the static page
Then I am shown a prompt
When I confirm the prompt
Then I should not see any static pages
......@@ -20,9 +18,16 @@ Feature: CMS.Static Pages
# Safari won't update the name properly
@skip_safari
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
Given I have created a static page
When I "edit" the static page
And I change the name to "New"
Then I should see a static page named "New"
# Safari won't update the name properly
@skip_safari
Scenario: Users can reorder static pages
Given I have created two different static pages
When I reorder the tabs
Then the tabs are in the reverse order
And I reload the page
Then the tabs are in the reverse order
......@@ -48,3 +48,47 @@ def change_name(step, new_name):
world.trigger_event(input_css)
save_button = 'a.save-button'
world.css_click(save_button)
@step(u'I reorder the tabs')
def reorder_tabs(_step):
# For some reason, the drag_and_drop method did not work in this case.
draggables = world.css_find('.drag-handle')
source = draggables.first
target = draggables.last
source.action_chains.click_and_hold(source._element).perform()
source.action_chains.move_to_element_with_offset(target._element, 0, 50).perform()
source.action_chains.release().perform()
@step(u'I have created a static page')
def create_static_page(step):
step.given('I have opened a new course in Studio')
step.given('I go to the static pages page')
step.given('I add a new page')
@step(u'I have created two different static pages')
def create_two_pages(step):
step.given('I have created a static page')
step.given('I "edit" the static page')
step.given('I change the name to "First"')
step.given('I add a new page')
# Verify order of tabs
_verify_tab_names('First', 'Empty')
@step(u'the tabs are in the reverse order')
def tabs_in_reverse_order(step):
_verify_tab_names('Empty', 'First')
def _verify_tab_names(first, second):
world.wait_for(
func=lambda _: len(world.css_find('.xmodule_StaticTabModule')) == 2,
timeout=200,
timeout_msg="Timed out waiting for two tabs to be present"
)
tabs = world.css_find('.xmodule_StaticTabModule')
assert tabs[0].text == first
assert tabs[1].text == second
......@@ -641,6 +641,7 @@ Feature: Video Component Editor
And I save changes
Then when I view the video it does show the captions
And I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
......@@ -652,3 +653,25 @@ Feature: Video Component Editor
Then when I view the video it does show the captions
And I see "LILA FISHER: Hi, welcome to Edx." text in the captions
#35
Scenario: After reverting Transcripts field in the Advanced tab "not found" message should be visible
Given I have created a Video component
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "not found"
And I upload the transcripts file "chinese_transcripts.srt"
Then I see status message "uploaded_successfully"
And I save changes
Then I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
And I revert the transcript field"HTML5 Transcript"
And I save changes
Then when I view the video it does not show the captions
And I edit the component
Then I see status message "not found"
......@@ -9,7 +9,7 @@ from django.conf import settings
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
from splinter.request_handler.request_handler import RequestHandler
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
......@@ -49,6 +49,13 @@ TRANSCRIPTS_BUTTONS = {
}
def _clear_field(index):
world.css_fill(SELECTORS['url_inputs'], '', index)
# In some reason chromeDriver doesn't trigger 'input' event after filling
# field by an empty value. That's why we trigger it manually via jQuery.
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
@step('I clear fields$')
def clear_fields(_step):
js_str = '''
......@@ -60,16 +67,18 @@ def clear_fields(_step):
for index in range(1, 4):
js = js_str.format(selector=SELECTORS['url_inputs'], index=index - 1)
world.browser.execute_script(js)
_step.given('I clear field number {0}'.format(index))
_clear_field(index)
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)
# In some reason chromeDriver doesn't trigger 'input' event after filling
# field by an empty value. That's why we trigger it manually via jQuery.
world.trigger_event(SELECTORS['url_inputs'], event='input', index=index)
_clear_field(index)
world.wait(DELAY)
world.wait_for_ajax_complete()
@step('I expect (.+) inputs are disabled$')
......@@ -91,39 +100,32 @@ def inputs_are_enabled(_step):
@step('I do not see error message$')
def i_do_not_see_error_message(_step):
world.wait(DELAY)
assert not world.css_visible(SELECTORS['error_bar'])
@step('I see error message "([^"]*)"$')
def i_see_error_message(_step, error):
world.wait(DELAY)
assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error.strip()])
@step('I do not see status message$')
def i_do_not_see_status_message(_step):
world.wait(DELAY)
world.wait_for_ajax_complete()
assert not world.css_visible(SELECTORS['status_bar'])
@step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status):
world.wait(DELAY)
world.wait_for_ajax_complete()
assert not world.css_visible(SELECTORS['error_bar'])
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status.strip()])
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):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
if not_see.strip():
......@@ -134,9 +136,6 @@ def i_see_button(_step, not_see, button_type):
@step('I (.*)see (.*)button "([^"]*)" number (\d+)$')
def i_see_button_with_custom_text(_step, not_see, button_type, custom_text, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
custom_text = custom_text.strip()
index = int(index.strip()) - 1
......@@ -149,22 +148,18 @@ def i_see_button_with_custom_text(_step, not_see, button_type, custom_text, inde
@step('I click transcript button "([^"]*)"$')
def click_button_transcripts_variant(_step, button_type):
world.wait(DELAY)
world.wait_for_ajax_complete()
button = button_type.strip()
world.css_click(TRANSCRIPTS_BUTTONS[button][0])
world.wait_for_ajax_complete()
@step('I click transcript button "([^"]*)" number (\d+)$')
def click_button_index(_step, button_type, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
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')
......@@ -186,9 +181,6 @@ def remove_transcripts_from_store(_step, subs_id):
@step('I enter a "([^"]+)" source to field number (\d+)$')
def i_enter_a_source(_step, link, index):
world.wait(DELAY)
world.wait_for_ajax_complete()
index = int(index) - 1
if index is not 0 and not world.css_visible(SELECTORS['collapse_bar']):
......@@ -197,6 +189,8 @@ def i_enter_a_source(_step, link, index):
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 "([^"]*)"$')
......@@ -204,6 +198,7 @@ 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('file', os.path.abspath(path))
world.wait_for_ajax_complete()
@step('I see "([^"]*)" text in the captions')
......@@ -213,9 +208,6 @@ def check_text_in_the_captions(_step, text):
@step('I see value "([^"]*)" in the field "([^"]*)"$')
def check_transcripts_field(_step, values, field_name):
world.wait(DELAY)
world.wait_for_ajax_complete()
world.click_link_by_text('Advanced')
field_id = '#' + world.browser.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('|')]
......@@ -225,22 +217,34 @@ def check_transcripts_field(_step, values, field_name):
@step('I save changes$')
def save_changes(_step):
world.wait(DELAY)
world.wait_for_ajax_complete()
save_css = 'a.save-button'
world.css_click(save_css)
world.wait_for_ajax_complete()
@step('I open tab "([^"]*)"$')
def open_tab(_step, tab_name):
world.click_link_by_text(tab_name.strip())
world.wait_for_ajax_complete()
@step('I set value "([^"]*)" to the field "([^"]*)"$')
def set_value_transcripts_field(_step, value, field_name):
world.wait(DELAY)
world.wait_for_ajax_complete()
field_id = '#' + world.browser.find_by_xpath('//label[text()="%s"]' % field_name.strip())[0]['for']
world.css_fill(field_id, value.strip())
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()
......@@ -53,6 +53,8 @@ Feature: CMS.Video Component
Then Captions become "invisible"
# 8
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: Open captions never become invisible
Given I have created a Video component with subtitles
And Make sure captions are open
......@@ -63,6 +65,8 @@ Feature: CMS.Video Component
Then Captions are "visible"
# 9
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: Closed captions are invisible when mouse doesn't hover on CC button
Given I have created a Video component with subtitles
And Make sure captions are closed
......@@ -71,6 +75,8 @@ Feature: CMS.Video Component
Then Captions are "invisible"
# 10
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: When enter key is pressed on a caption shows an outline around it
Given I have created a Video component with subtitles
And Make sure captions are opened
......
......@@ -181,7 +181,7 @@ def click_on_the_caption(_step, index):
@step('I see caption line with data-index "([^"]*)" has class "([^"]*)"$')
def caption_line_has_class(_step, index, className):
SELECTOR = ".subtitles > li[data-index='{index}']".format(index=int(index.strip()))
world.css_has_class(SELECTOR, className.strip())
assert world.css_has_class(SELECTOR, className.strip())
@step('I see a range on slider$')
......
......@@ -3,7 +3,6 @@ Unit tests for getting the list of courses and the course outline.
"""
import json
import lxml
from django.core.urlresolvers import reverse
from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.django import loc_mapper
......@@ -60,8 +59,7 @@ class TestCourseIndex(CourseTestCase):
"""
Test the error conditions for the access
"""
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True)
outline_url = locator.url_reverse('course/', '')
outline_url = self.course_locator.url_reverse('course/', '')
# register a non-staff member and try to delete the course branch
non_staff_client, _ = self.createNonStaffAuthedUserClient()
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
......
......@@ -263,7 +263,7 @@ class ExportTestCase(CourseTestCase):
parent_location=vertical.location,
category='aawefawef'
)
self._verify_export_failure('/edit/i4x://MITx/999/vertical/foo')
self._verify_export_failure(u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/foo')
def _verify_export_failure(self, expectedText):
""" Export failure helper method. """
......
......@@ -9,6 +9,7 @@ from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore.exceptions import ItemNotFoundError
class ItemTest(CourseTestCase):
......@@ -30,7 +31,7 @@ class ItemTest(CourseTestCase):
"""
Get the item referenced by the locator from the modulestore
"""
store = modulestore('draft') if draft else modulestore()
store = modulestore('draft') if draft else modulestore('direct')
return store.get_item(self.get_old_id(locator))
def response_locator(self, response):
......@@ -251,3 +252,105 @@ class TestEditItem(ItemTest):
self.assertEqual(self.get_old_id(self.problem_locator).url(), children[0])
self.assertEqual(self.get_old_id(unit1_locator).url(), children[2])
self.assertEqual(self.get_old_id(unit2_locator).url(), children[1])
def test_make_public(self):
""" Test making a private problem public (publishing it). """
# When the problem is first created, it is only in draft (because of its category).
with self.assertRaises(ItemNotFoundError):
self.get_item_from_modulestore(self.problem_locator, False)
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
def test_make_private(self):
""" Test making a public problem private (un-publishing it). """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
# Now make it private
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_private'}
)
with self.assertRaises(ItemNotFoundError):
self.get_item_from_modulestore(self.problem_locator, False)
def test_make_draft(self):
""" Test creating a draft version of a public problem. """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
# Now make it draft, which means both versions will exist.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'create_draft'}
)
# Update the draft version and check that published is different.
self.client.ajax_post(
self.problem_update_url,
data={'metadata': {'due': '2077-10-10T04:00Z'}}
)
published = self.get_item_from_modulestore(self.problem_locator, False)
self.assertIsNone(published.due)
draft = self.get_item_from_modulestore(self.problem_locator, True)
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
def test_make_public_with_update(self):
""" Update a problem and make it public at the same time. """
self.client.ajax_post(
self.problem_update_url,
data={
'metadata': {'due': '2077-10-10T04:00Z'},
'publish': 'make_public'
}
)
published = self.get_item_from_modulestore(self.problem_locator, False)
self.assertEqual(published.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
def test_make_private_with_update(self):
""" Make a problem private and update it at the same time. """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.client.ajax_post(
self.problem_update_url,
data={
'metadata': {'due': '2077-10-10T04:00Z'},
'publish': 'make_private'
}
)
with self.assertRaises(ItemNotFoundError):
self.get_item_from_modulestore(self.problem_locator, False)
draft = self.get_item_from_modulestore(self.problem_locator, True)
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
def test_create_draft_with_update(self):
""" Create a draft and update it at the same time. """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
# Now make it draft, which means both versions will exist.
self.client.ajax_post(
self.problem_update_url,
data={
'metadata': {'due': '2077-10-10T04:00Z'},
'publish': 'create_draft'
}
)
published = self.get_item_from_modulestore(self.problem_locator, False)
self.assertIsNone(published.due)
draft = self.get_item_from_modulestore(self.problem_locator, True)
self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
"""
Test CRUD for authorization.
"""
from django.test.utils import override_settings
from django.contrib.auth.models import User, Group
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from contentstore.tests.utils import AjaxEnabledTestClient
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore import Location
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME
from auth import authz
import copy
from contentstore.views.access import has_access
@override_settings(MODULESTORE=TEST_MODULESTORE)
class TestCourseAccess(ModuleStoreTestCase):
"""
Course-based access (as opposed to access of a non-course xblock)
"""
def setUp(self):
"""
Create a staff user and log them in (creating the client).
Create a pool of users w/o granting them any permissions
"""
super(TestCourseAccess, self).setUp()
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(uname, email, password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
self.user.save()
self.client = AjaxEnabledTestClient()
self.client.login(username=uname, password=password)
# create a course via the view handler which has a different strategy for permissions than the factory
self.course_location = Location(['i4x', 'myu', 'mydept.mycourse', 'course', 'myrun'])
self.course_locator = loc_mapper().translate_location(
self.course_location.course_id, self.course_location, False, True
)
self.client.ajax_post(
self.course_locator.url_reverse('course'),
{
'org': self.course_location.org,
'number': self.course_location.course,
'display_name': 'My favorite course',
'run': self.course_location.name,
}
)
self.users = self._create_users()
def _create_users(self):
"""
Create 8 users and return them
"""
users = []
for i in range(8):
username = "user{}".format(i)
email = "test+user{}@edx.org".format(i)
user = User.objects.create_user(username, email, 'foo')
user.is_active = True
user.save()
users.append(user)
return users
def tearDown(self):
"""
Reverse the setup
"""
self.client.logout()
ModuleStoreTestCase.tearDown(self)
def test_get_all_users(self):
"""
Test getting all authors for a course where their permissions run the gamut of allowed group
types.
"""
# first check the groupname for the course creator.
self.assertTrue(
self.user.groups.filter(
name="{}_{}".format(INSTRUCTOR_ROLE_NAME, self.course_locator.course_id)
).exists(),
"Didn't add creator as instructor."
)
users = copy.copy(self.users)
user_by_role = {}
# add the misc users to the course in different groups
for role in [INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME]:
user_by_role[role] = []
groupnames, _ = authz.get_all_course_role_groupnames(self.course_locator, role)
for groupname in groupnames:
group, _ = Group.objects.get_or_create(name=groupname)
user = users.pop()
user_by_role[role].append(user)
user.groups.add(group)
user.save()
self.assertTrue(has_access(user, self.course_locator), "{} does not have access".format(user))
self.assertTrue(has_access(user, self.course_location), "{} does not have access".format(user))
response = self.client.get_html(self.course_locator.url_reverse('course_team'))
for role in [INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME]:
for user in user_by_role[role]:
self.assertContains(response, user.email)
# test copying course permissions
copy_course_location = Location(['i4x', 'copyu', 'copydept.mycourse', 'course', 'myrun'])
copy_course_locator = loc_mapper().translate_location(
copy_course_location.course_id, copy_course_location, False, True
)
# pylint: disable=protected-access
authz._copy_course_group(self.course_locator, copy_course_locator)
for role in [INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME]:
for user in user_by_role[role]:
self.assertTrue(has_access(user, copy_course_locator), "{} no copy access".format(user))
self.assertTrue(has_access(user, copy_course_location), "{} no copy access".format(user))
\ No newline at end of file
......@@ -20,7 +20,7 @@ class CMSLogTest(TestCase):
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_SQL_TRACKING_LOGS': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_SQL_TRACKING_LOGS': True}):
for request_params in requests:
response = self.client.post(reverse(cms_user_track), request_params)
self.assertEqual(response.status_code, 204)
......@@ -34,7 +34,7 @@ class CMSLogTest(TestCase):
{"event": "my_event", "event_type": "my_event_type", "page": "my_page"},
{"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"}
]
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_SQL_TRACKING_LOGS': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_SQL_TRACKING_LOGS': True}):
for request_params in requests:
response = self.client.get(reverse(cms_user_track), request_params)
self.assertEqual(response.status_code, 204)
......@@ -29,8 +29,8 @@ class UsersTestCase(CourseTestCase):
self.detail_url = self.location.url_reverse('course_team', self.ext_user.email)
self.inactive_detail_url = self.location.url_reverse('course_team', self.inactive_user.email)
self.invalid_detail_url = self.location.url_reverse('course_team', "nonexistent@user.com")
self.staff_groupname = get_course_groupname_for_role(self.course.location, "staff")
self.inst_groupname = get_course_groupname_for_role(self.course.location, "instructor")
self.staff_groupname = get_course_groupname_for_role(self.course_locator, "staff")
self.inst_groupname = get_course_groupname_for_role(self.course_locator, "instructor")
def test_index(self):
resp = self.client.get(self.index_url, HTTP_ACCEPT='text/html')
......@@ -145,18 +145,6 @@ class UsersTestCase(CourseTestCase):
self.assertIn("error", result)
self.assert_not_enrolled()
def test_detail_post_bad_json(self):
resp = self.client.post(
self.detail_url,
data="{foo}",
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
self.assert_not_enrolled()
def test_detail_post_no_json(self):
resp = self.client.post(
self.detail_url,
......
......@@ -28,33 +28,33 @@ class LMSLinksTestCase(TestCase):
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def about_page_marketing_site_test(self):
""" Get URL for about page, marketing root present. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//dummy-root/courses/mitX/101/test/about")
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "//localhost:8000/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'http://www.dummy'})
def about_page_marketing_site_remove_http_test(self):
""" Get URL for about page, marketing root present, remove http://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummy'})
def about_page_marketing_site_remove_https_test(self):
""" Get URL for about page, marketing root present, remove https://. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummy/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={'ROOT': 'www.dummyhttps://x'})
def about_page_marketing_site_https__edge_test(self):
""" Get URL for about page, only remove https:// at the beginning of the string. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "//www.dummyhttps://x/courses/mitX/101/test/about")
@override_settings(MKTG_URLS={})
def about_page_marketing_urls_not_set_test(self):
""" Error case. ENABLE_MKTG_SITE is True, but there is either no MKTG_URLS, or no MKTG_URLS Root property. """
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), None)
@override_settings(LMS_BASE=None)
......
......@@ -10,8 +10,9 @@ from django.test.client import Client
from django.test.utils import override_settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.django import loc_mapper
def parse_json(response):
......@@ -41,6 +42,7 @@ class AjaxEnabledTestClient(Client):
if not isinstance(data, basestring):
data = json.dumps(data or {})
kwargs.setdefault("HTTP_X_REQUESTED_WITH", "XMLHttpRequest")
kwargs.setdefault("HTTP_ACCEPT", "application/json")
return self.post(path=path, data=data, content_type=content_type, **kwargs)
def get_html(self, path, data=None, follow=False, **extra):
......@@ -88,6 +90,9 @@ class CourseTestCase(ModuleStoreTestCase):
display_name='Robot Super Course',
)
self.course_location = self.course.location
self.course_locator = loc_mapper().translate_location(
self.course.location.course_id, self.course.location, False, True
)
def createNonStaffAuthedUserClient(self):
"""
......@@ -106,3 +111,16 @@ class CourseTestCase(ModuleStoreTestCase):
client = Client()
client.login(username=uname, password=password)
return client, nonstaff
def populateCourse(self):
"""
Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2)
"""
def descend(parent, stack):
xblock_type = stack.pop(0)
for _ in range(2):
child = ItemFactory.create(category=xblock_type, parent_location=parent.location)
if stack:
descend(child, stack)
descend(self.course, ['chapter', 'sequential', 'vertical', 'problem'])
......@@ -196,6 +196,7 @@ def remove_subs_from_store(subs_id, item):
try:
content = contentstore().find(content_location)
contentstore().delete(content.get_id())
del_cached_content(content.location)
log.info("Removed subs %s from store", subs_id)
except NotFoundError:
pass
......@@ -310,7 +311,9 @@ def manage_video_subtitles_save(old_item, new_item):
Video player item has some video fields: HTML5 ones and Youtube one.
1. If value of `sub` field of `new_item` is different from values of video fields of `new_item`,
If value of `sub` field of `new_item` is cleared, transcripts should be removed.
If value of `sub` field of `new_item` is different from values of video fields of `new_item`,
and `new_item.sub` file is present, then code in this function creates copies of
`new_item.sub` file with new names. That names are equal to values of video fields of `new_item`
After that `sub` field of `new_item` is changed to one of values of video fields.
......@@ -328,6 +331,9 @@ def manage_video_subtitles_save(old_item, new_item):
for video_id in possible_video_id_list:
if not video_id:
continue
if not sub_name:
remove_subs_from_store(video_id, new_item)
continue
# copy_or_rename_transcript changes item.sub of module
try:
# updates item.sub with `video_id`, if it is successful.
......
......@@ -136,7 +136,7 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
if settings.LMS_BASE is not None:
if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
else:
lms_base = settings.LMS_BASE
......@@ -155,7 +155,7 @@ def get_lms_link_for_about_page(location):
"""
Returns the url to the course about page from the location tuple.
"""
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False):
if settings.FEATURES.get('ENABLE_MKTG_SITE', False):
if not hasattr(settings, 'MKTG_URLS'):
log.exception("ENABLE_MKTG_SITE is True, but MKTG_URLS is not defined.")
about_base = None
......
......@@ -7,7 +7,7 @@ from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from xmodule.contentstore.django import contentstore
......
......@@ -5,9 +5,8 @@ from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from django.core.urlresolvers import reverse
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
from django.http import HttpResponseNotFound
from django.core.exceptions import PermissionDenied
from xmodule.modulestore.django import loc_mapper
......@@ -22,6 +21,8 @@ from xmodule.modulestore.locator import BlockUsageLocator
__all__ = ['checklists_handler']
# pylint: disable=unused-argument
@require_http_methods(("GET", "POST", "PUT"))
@login_required
@ensure_csrf_cookie
......@@ -85,8 +86,8 @@ def checklists_handler(request, tag=None, course_id=None, branch=None, version_g
return JsonResponse(expanded_checklist)
else:
return HttpResponseBadRequest(
( "Could not save checklist state because the checklist index "
"was out of range or unspecified."),
("Could not save checklist state because the checklist index "
"was out of range or unspecified."),
content_type="text/plain"
)
else:
......@@ -113,14 +114,12 @@ def expand_checklist_action_url(course_module, checklist):
The method does a copy of the input checklist and does not modify the input argument.
"""
expanded_checklist = copy.deepcopy(checklist)
oldurlconf_map = {
"SettingsDetails": "settings_details",
"SettingsGrading": "settings_grading"
}
urlconf_map = {
"ManageUsers": "course_team",
"CourseOutline": "course"
"CourseOutline": "course",
"SettingsDetails": "settings/details",
"SettingsGrading": "settings/grading",
}
for item in expanded_checklist.get('items'):
......@@ -130,12 +129,5 @@ def expand_checklist_action_url(course_module, checklist):
ctx_loc = course_module.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
item['action_url'] = location.url_reverse(url_prefix, '')
elif action_url in oldurlconf_map:
urlconf_name = oldurlconf_map[action_url]
item['action_url'] = reverse(urlconf_name, kwargs={
'org': course_module.location.org,
'course': course_module.location.course,
'name': course_module.location.name,
})
return expanded_checklist
......@@ -4,7 +4,7 @@ These views will NOT be shown on production: trying to access them will result
in a 404 error.
"""
# pylint: disable=W0613
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
def dev_mode(request):
......
......@@ -2,7 +2,7 @@
from django.http import (HttpResponse, HttpResponseServerError,
HttpResponseNotFound)
from mitxmako.shortcuts import render_to_string, render_to_response
from edxmako.shortcuts import render_to_string, render_to_response
import functools
import json
......
from django.http import HttpResponse
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_string, render_to_response
from edxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
......
......@@ -14,7 +14,6 @@ from django.conf import settings
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.core.exceptions import SuspiciousOperation, PermissionDenied
......@@ -22,7 +21,7 @@ from django.http import HttpResponseNotFound
from django.views.decorators.http import require_http_methods, require_GET
from django.utils.translation import ugettext as _
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
from auth.authz import create_all_course_groups
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -140,7 +139,7 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
"size": size,
"deleteUrl": "",
"deleteType": "",
"url": location.url_reverse('import/', ''),
"url": location.url_reverse('import'),
"thumbnailUrl": ""
}]
})
......@@ -252,8 +251,8 @@ def import_handler(request, tag=None, course_id=None, branch=None, version_guid=
course_module = modulestore().get_item(old_location)
return render_to_response('import.html', {
'context_course': course_module,
'successful_import_redirect_url': location.url_reverse("course/", ""),
'import_status_url': location.url_reverse("import_status/", "fillerName"),
'successful_import_redirect_url': location.url_reverse("course"),
'import_status_url': location.url_reverse("import_status", "fillerName"),
})
else:
return HttpResponseNotFound()
......@@ -313,7 +312,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# an _accept URL parameter will be preferred over HTTP_ACCEPT in the header.
requested_format = request.REQUEST.get('_accept', request.META.get('HTTP_ACCEPT', 'text/html'))
export_url = location.url_reverse('export/', '') + '?_accept=application/x-tgz'
export_url = location.url_reverse('export') + '?_accept=application/x-tgz'
if 'application/x-tgz' in requested_format:
name = old_location.name
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
......@@ -339,16 +338,16 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
# if we have a nested exception, then we'll show the more generic error message
pass
unit_locator = loc_mapper().translate_location(old_location.course_id, parent.location, False, True)
return render_to_response('export.html', {
'context_course': course_module,
'in_err': True,
'raw_err_msg': str(e),
'failed_module': failed_item,
'unit': unit,
'edit_unit_url': reverse('edit_unit', kwargs={
'location': parent.location
}) if parent else '',
'course_home_url': location.url_reverse("course/", ""),
'edit_unit_url': unit_locator.url_reverse("unit") if parent else "",
'course_home_url': location.url_reverse("course"),
'export_url': export_url
})
......@@ -359,7 +358,7 @@ def export_handler(request, tag=None, course_id=None, branch=None, version_guid=
'in_err': True,
'unit': None,
'raw_err_msg': str(e),
'course_home_url': location.url_reverse("course/", ""),
'course_home_url': location.url_reverse("course"),
'export_url': export_url
})
......
......@@ -3,7 +3,9 @@
import logging
from uuid import uuid4
from functools import partial
from static_replace import replace_static_urls
from xmodule_modifiers import wrap_xblock
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
......@@ -27,6 +29,9 @@ from xmodule.modulestore.locator import BlockUsageLocator
from student.models import CourseEnrollment
from django.http import HttpResponseBadRequest
from xblock.fields import Scope
from preview import handler_prefix, get_preview_html
from edxmako.shortcuts import render_to_response, render_to_string
from models.settings.course_grading import CourseGradingModel
__all__ = ['orphan_handler', 'xblock_handler']
......@@ -51,17 +56,21 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
all children and "all_versions" to delete from all (mongo) versions.
GET
json: returns representation of the xblock (locator id, data, and metadata).
if ?fields=graderType, it returns the graderType for the unit instead of the above.
html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
PUT or POST
json: if xblock location is specified, update the xblock instance. The json payload can contain
json: if xblock locator is specified, update the xblock instance. The json payload can contain
these fields, all optional:
:data: the new value for the data.
:children: the locator ids of children for this xblock.
:metadata: new values for the metadata fields. Any whose values are None will be deleted not set
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
:graderType: change how this unit is graded
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
The JSON representation on the updated xblock (minus children) is returned.
if xblock location is not specified, create a new xblock instance. The json playload can contain
if xblock locator is not specified, create a new xblock instance. The json playload can contain
these fields:
:parent_locator: parent for new xblock, required
:category: type of xblock, required
......@@ -70,14 +79,38 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
The locator (and old-style id) for the created xblock (minus children) is returned.
"""
if course_id is not None:
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, location):
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, locator):
raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location)
old_location = loc_mapper().translate_locator_to_location(locator)
if request.method == 'GET':
rsp = _get_module_info(location)
return JsonResponse(rsp)
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component),
'editor': content
})
elif request.method == 'DELETE':
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
......@@ -85,12 +118,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
return _delete_item_at_location(old_location, delete_children, delete_all_versions)
else: # Since we have a course_id, we are updating an existing xblock.
return _save_item(
location,
request,
locator,
old_location,
data=request.json.get('data'),
children=request.json.get('children'),
metadata=request.json.get('metadata'),
nullout=request.json.get('nullout')
nullout=request.json.get('nullout'),
grader_type=request.json.get('graderType'),
publish=request.json.get('publish'),
)
elif request.method in ('PUT', 'POST'):
return _create_item(request)
......@@ -101,11 +137,14 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
)
def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None):
def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None,
grader_type=None, publish=None):
"""
Saves certain properties (data, children, metadata, nullout) for a given xblock item.
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
to default).
The item_location is still the old-style location.
The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator
"""
store = get_modulestore(item_location)
......@@ -123,6 +162,14 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
if publish:
if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
elif publish == 'create_draft':
# This clones the existing item location to a draft location (the draft is
# implicit, because modulestore is a Draft modulestore)
modulestore().convert_to_draft(item_location)
if data:
store.update_item(item_location, data)
else:
......@@ -170,12 +217,25 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
if existing_item.category == 'video':
manage_video_subtitles_save(existing_item, existing_item)
# Note that children aren't being returned until we have a use case.
return JsonResponse({
result = {
'id': unicode(usage_loc),
'data': data,
'metadata': own_metadata(existing_item)
})
}
if grader_type is not None:
result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type))
# Make public after updating the xblock, in case the caller asked
# for both an update and a publish.
if publish and publish == 'make_public':
_xmodule_recurse(
existing_item,
lambda i: modulestore().publish(i.location, request.user.id)
)
# Note that children aren't being returned until we have a use case.
return JsonResponse(result)
@login_required
......@@ -192,10 +252,7 @@ def _create_item(request):
raise PermissionDenied()
parent = get_modulestore(category).get_item(parent_location)
# Necessary to set revision=None or else metadata inheritance does not work
# (the ID with @draft will be used as the key in the inherited metadata map,
# and that is not expected by the code that later references it).
dest_location = parent_location.replace(category=category, name=uuid4().hex, revision=None)
dest_location = parent_location.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the request
metadata = {}
......@@ -224,7 +281,7 @@ def _create_item(request):
course_location = loc_mapper().translate_locator_to_location(parent_locator, get_course=True)
locator = loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
return JsonResponse({'id': dest_location.url(), "locator": unicode(locator)})
return JsonResponse({"locator": unicode(locator)})
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False):
......
......@@ -3,9 +3,9 @@ from functools import partial
from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response, render_to_string
from edxmako.shortcuts import render_to_response, render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xblock
from xmodule.error_module import ErrorDescriptor
......@@ -24,10 +24,9 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .helpers import render_from_lms
from .access import has_access
from ..utils import get_course_for_item
__all__ = ['preview_handler', 'preview_component']
__all__ = ['preview_handler']
log = logging.getLogger(__name__)
......@@ -53,13 +52,13 @@ def preview_handler(request, usage_id, handler, suffix=''):
usage_id: The usage-id of the block to dispatch to, passed through `quote_slashes`
handler: The handler to execute
suffix: The remaineder of the url to be passed to the handler
suffix: The remainder of the url to be passed to the handler
"""
location = unquote_slashes(usage_id)
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, descriptor)
instance = _load_preview_module(request, descriptor)
# Let the module handle the AJAX
req = django_to_webob_request(request)
try:
......@@ -85,32 +84,6 @@ def preview_handler(request, usage_id, handler, suffix=''):
return webob_to_django_response(resp)
@login_required
def preview_component(request, location):
"Return the HTML preview of a component"
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
return HttpResponseForbidden()
component = modulestore().get_item(location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix))
try:
content = component.render('studio_view').content
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=W0703
content = render_to_string('html_error.html', {'message': str(exc)})
return render_to_response('component.html', {
'preview': get_preview_html(request, component),
'editor': content
})
class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
"""
An XModule ModuleSystem for use in Studio previews
......@@ -119,7 +92,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
return handler_prefix(block, handler_name, suffix) + '?' + query
def preview_module_system(request, descriptor):
def _preview_module_system(request, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
......@@ -135,7 +108,7 @@ def preview_module_system(request, descriptor):
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda event_type, event: None,
filestore=descriptor.runtime.resources_fs,
get_module=partial(load_preview_module, request),
get_module=partial(_load_preview_module, request),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
......@@ -162,7 +135,7 @@ def preview_module_system(request, descriptor):
)
def load_preview_module(request, descriptor):
def _load_preview_module(request, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor.
......@@ -171,7 +144,7 @@ def load_preview_module(request, descriptor):
"""
student_data = DbModel(SessionKeyValueStore(request))
descriptor.bind_for_student(
preview_module_system(request, descriptor),
_preview_module_system(request, descriptor),
LmsFieldData(descriptor._field_data, student_data), # pylint: disable=protected-access
)
return descriptor
......@@ -182,7 +155,7 @@ def get_preview_html(request, descriptor):
Returns the HTML returned by the XModule's student_view,
specified by the descriptor and idx.
"""
module = load_preview_module(request, descriptor)
module = _load_preview_module(request, descriptor)
try:
content = module.render("student_view").content
except Exception as exc: # pylint: disable=W0703
......
......@@ -6,11 +6,11 @@ from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks']
__all__ = ['signup', 'login_page', 'howitworks']
@ensure_csrf_cookie
......@@ -22,13 +22,6 @@ def signup(request):
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
......
......@@ -18,11 +18,12 @@ from django.conf import settings
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError
from util.json_request import JsonResponse
from xmodule.modulestore.locator import BlockUsageLocator
from ..transcripts_utils import (
generate_subs_from_source,
......@@ -77,20 +78,14 @@ def upload_transcripts(request):
'subs': '',
}
item_location = request.POST.get('id')
if not item_location:
return error_response(response, 'POST data without "id" form data.')
locator = request.POST.get('locator')
if not locator:
return error_response(response, 'POST data without "locator" form data.')
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
return error_response(response, "Can't find item by location.")
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
item = _get_item(request, request.POST)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
return error_response(response, "Can't find item by locator.")
if 'file' not in request.FILES:
return error_response(response, 'POST data without "file" form data.')
......@@ -156,23 +151,17 @@ def download_transcripts(request):
Raises Http404 if unsuccessful.
"""
item_location = request.GET.get('id')
if not item_location:
log.debug('GET data without "id" property.')
locator = request.GET.get('locator')
if not locator:
log.debug('GET data without "locator" property.')
raise Http404
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
log.debug("Can't find item by location.")
item = _get_item(request, request.GET)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
log.debug("Can't find item by locator.")
raise Http404
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
subs_id = request.GET.get('subs_id')
if not subs_id:
log.debug('GET data without "subs_id" property.')
......@@ -240,7 +229,7 @@ def check_transcripts(request):
'status': 'Error',
}
try:
__, videos, item = validate_transcripts_data(request)
__, videos, item = _validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(transcripts_presence, e.message)
......@@ -303,7 +292,7 @@ def check_transcripts(request):
if len(html5_subs) == 2: # check html5 transcripts for equality
transcripts_presence['html5_equal'] = json.loads(html5_subs[0]) == json.loads(html5_subs[1])
command, subs_to_use = transcripts_logic(transcripts_presence, videos)
command, subs_to_use = _transcripts_logic(transcripts_presence, videos)
transcripts_presence.update({
'command': command,
'subs': subs_to_use,
......@@ -311,7 +300,7 @@ def check_transcripts(request):
return JsonResponse(transcripts_presence)
def transcripts_logic(transcripts_presence, videos):
def _transcripts_logic(transcripts_presence, videos):
"""
By `transcripts_presence` content, figure what show to user:
......@@ -386,7 +375,7 @@ def choose_transcripts(request):
}
try:
data, videos, item = validate_transcripts_data(request)
data, videos, item = _validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
......@@ -416,7 +405,7 @@ def replace_transcripts(request):
response = {'status': 'Error', 'subs': ''}
try:
__, videos, item = validate_transcripts_data(request)
__, videos, item = _validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
......@@ -435,7 +424,7 @@ def replace_transcripts(request):
return JsonResponse(response)
def validate_transcripts_data(request):
def _validate_transcripts_data(request):
"""
Validates, that request contains all proper data for transcripts processing.
......@@ -452,18 +441,10 @@ def validate_transcripts_data(request):
if not data:
raise TranscriptsRequestValidationException('Incoming video data is empty.')
item_location = data.get('id')
# This is placed before has_access() to validate item_location,
# because has_access() raises InvalidLocationError if location is invalid.
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
raise TranscriptsRequestValidationException("Can't find item by location.")
# Check permissions for this user within this course.
if not has_access(request.user, item_location):
raise PermissionDenied()
item = _get_item(request, data)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
raise TranscriptsRequestValidationException("Can't find item by locator.")
if item.category != 'video':
raise TranscriptsRequestValidationException('Transcripts are supported only for "video" modules.')
......@@ -492,7 +473,7 @@ def rename_transcripts(request):
response = {'status': 'Error', 'subs': ''}
try:
__, videos, item = validate_transcripts_data(request)
__, videos, item = _validate_transcripts_data(request)
except TranscriptsRequestValidationException as e:
return error_response(response, e.message)
......@@ -525,11 +506,10 @@ def save_transcripts(request):
if not data:
return error_response(response, 'Incoming video data is empty.')
item_location = data.get('id')
try:
item = modulestore().get_item(item_location)
except (ItemNotFoundError, InvalidLocationError):
return error_response(response, "Can't find item by location.")
item = _get_item(request, data)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError):
return error_response(response, "Can't find item by locator.")
metadata = data.get('metadata')
if metadata is not None:
......@@ -553,3 +533,24 @@ def save_transcripts(request):
response['status'] = 'Success'
return JsonResponse(response)
def _get_item(request, data):
"""
Obtains from 'data' the locator for an item.
Next, gets that item from the modulestore (allowing any errors to raise up).
Finally, verifies that the user has access to the item.
Returns the item.
"""
locator = BlockUsageLocator(data.get('locator'))
old_location = loc_mapper().translate_locator_to_location(locator)
# This is placed before has_access() to validate the location,
# because has_access() raises InvalidLocationError if location is invalid.
item = modulestore().get_item(old_location)
if not has_access(request.user, locator):
raise PermissionDenied()
return item
import json
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.contrib.auth.models import User, Group
from django.contrib.auth.decorators import login_required
......@@ -7,12 +6,14 @@ from django.views.decorators.http import require_http_methods
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore, loc_mapper
from util.json_request import JsonResponse
from util.json_request import JsonResponse, expect_json
from auth.authz import (
STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_course_groupname_for_role)
STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_course_groupname_for_role,
get_course_role_users
)
from course_creators.views import user_requested_access
from .access import has_access
......@@ -35,6 +36,7 @@ def request_course_creator(request):
return JsonResponse({"Status": "OK"})
# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
......@@ -62,38 +64,39 @@ def course_team_handler(request, tag=None, course_id=None, branch=None, version_
return HttpResponseNotFound()
def _manage_users(request, location):
def _manage_users(request, locator):
"""
This view will return all CMS users who are editors for the specified course
"""
old_location = loc_mapper().translate_locator_to_location(location)
old_location = loc_mapper().translate_locator_to_location(locator)
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
if not has_access(request.user, locator):
raise PermissionDenied()
course_module = modulestore().get_item(old_location)
staff_groupname = get_course_groupname_for_role(location, "staff")
staff_group, __ = Group.objects.get_or_create(name=staff_groupname)
inst_groupname = get_course_groupname_for_role(location, "instructor")
inst_group, __ = Group.objects.get_or_create(name=inst_groupname)
instructors = get_course_role_users(locator, INSTRUCTOR_ROLE_NAME)
# the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
staff = set(get_course_role_users(locator, STAFF_ROLE_NAME)).union(instructors)
return render_to_response('manage_users.html', {
'context_course': course_module,
'staff': staff_group.user_set.all(),
'instructors': inst_group.user_set.all(),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'staff': staff,
'instructors': instructors,
'allow_actions': has_access(request.user, locator, role=INSTRUCTOR_ROLE_NAME),
})
def _course_team_user(request, location, email):
old_location = loc_mapper().translate_locator_to_location(location)
@expect_json
def _course_team_user(request, locator, email):
"""
Handle the add, remove, promote, demote requests ensuring the requester has authority
"""
# check that logged in user has permissions to this item
if has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
if has_access(request.user, locator, role=INSTRUCTOR_ROLE_NAME):
# instructors have full permissions
pass
elif has_access(request.user, location, role=STAFF_ROLE_NAME) and email == request.user.email:
elif has_access(request.user, locator, role=STAFF_ROLE_NAME) and email == request.user.email:
# staff can only affect themselves
pass
else:
......@@ -123,7 +126,7 @@ def _course_team_user(request, location, email):
# what's the highest role that this user has?
groupnames = set(g.name for g in user.groups.all())
for role in roles:
role_groupname = get_course_groupname_for_role(old_location, role)
role_groupname = get_course_groupname_for_role(locator, role)
if role_groupname in groupnames:
msg["role"] = role
break
......@@ -139,7 +142,7 @@ def _course_team_user(request, location, email):
# make sure that the role groups exist
groups = {}
for role in roles:
groupname = get_course_groupname_for_role(old_location, role)
groupname = get_course_groupname_for_role(locator, role)
group, __ = Group.objects.get_or_create(name=groupname)
groups[role] = group
......@@ -162,22 +165,13 @@ def _course_team_user(request, location, email):
return JsonResponse()
# all other operations require the requesting user to specify a role
if request.META.get("CONTENT_TYPE", "").startswith("application/json") and request.body:
try:
payload = json.loads(request.body)
except:
return JsonResponse({"error": _("malformed JSON")}, 400)
try:
role = payload["role"]
except KeyError:
return JsonResponse({"error": _("`role` is required")}, 400)
else:
if not "role" in request.POST:
return JsonResponse({"error": _("`role` is required")}, 400)
role = request.POST["role"]
role = request.json.get("role", request.POST.get("role"))
if role is None:
return JsonResponse({"error": _("`role` is required")}, 400)
old_location = loc_mapper().translate_locator_to_location(locator)
if role == "instructor":
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
if not has_access(request.user, locator, role=INSTRUCTOR_ROLE_NAME):
msg = {
"error": _("Only instructors may create other instructors")
}
......@@ -203,4 +197,3 @@ def _course_team_user(request, location, email):
CourseEnrollment.enroll(user, old_location.course_id)
return JsonResponse()
......@@ -8,7 +8,7 @@ from course_creators.views import update_course_creator_group
from ratelimitbackend import admin
from django.conf import settings
from django.dispatch import receiver
from mitxmako.shortcuts import render_to_string
from edxmako.shortcuts import render_to_string
from django.core.mail import send_mail
from smtplib import SMTPException
......@@ -91,7 +91,7 @@ def send_user_notification_callback(sender, **kwargs):
user = kwargs['user']
updated_state = kwargs['state']
studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL', '')
studio_request_email = settings.FEATURES.get('STUDIO_REQUEST_EMAIL', '')
context = {'studio_request_email': studio_request_email}
subject = render_to_string('emails/course_creator_subject.txt', context)
......@@ -118,7 +118,7 @@ def send_admin_notification_callback(sender, **kwargs):
"""
user = kwargs['user']
studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL', '')
studio_request_email = settings.FEATURES.get('STUDIO_REQUEST_EMAIL', '')
context = {'user_name': user.username, 'user_email': user.email}
subject = render_to_string('emails/course_creator_admin_subject.txt', context)
......
......@@ -69,7 +69,7 @@ class CourseCreatorAdminTest(TestCase):
self.studio_request_email
)
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group_patch):
with mock.patch.dict('django.conf.settings.FEATURES', self.enable_creator_group_patch):
# User is initially unrequested.
self.assertFalse(is_user_in_creator_group(self.user))
......@@ -119,7 +119,7 @@ class CourseCreatorAdminTest(TestCase):
else:
self.assertEquals(base_num_emails, len(mail.outbox))
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group_patch):
with mock.patch.dict('django.conf.settings.FEATURES', self.enable_creator_group_patch):
# E-mail message should be sent to admin only when new state is PENDING, regardless of what
# previous state was (unless previous state was already PENDING).
# E-mail message sent to user only on transition into and out of GRANTED state.
......@@ -159,7 +159,7 @@ class CourseCreatorAdminTest(TestCase):
self.assertFalse(self.creator_admin.has_change_permission(self.request))
def test_rate_limit_login(self):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_CREATOR_GROUP': True}):
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREATOR_GROUP': True}):
post_params = {'username': self.user.username, 'password': 'wrong_password'}
# try logging in 30 times, the default limit in the number of failed
# login attempts in one 5 minute period before the rate gets limited
......
......@@ -46,7 +46,7 @@ class CourseCreatorView(TestCase):
self.assertEqual('unrequested', get_course_creator_status(self.user))
def test_add_granted(self):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
# Calling add_user_with_status_granted impacts is_user_in_course_group_role.
self.assertFalse(is_user_in_creator_group(self.user))
......@@ -60,7 +60,7 @@ class CourseCreatorView(TestCase):
self.assertTrue(is_user_in_creator_group(self.user))
def test_update_creator_group(self):
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {"ENABLE_CREATOR_GROUP": True}):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
self.assertFalse(is_user_in_creator_group(self.user))
update_course_creator_group(self.admin, self.user, True)
self.assertTrue(is_user_in_creator_group(self.user))
......
import re
import logging
import datetime
import json
from json.encoder import JSONEncoder
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
from contentstore.utils import get_modulestore, course_image_url
from models.settings import course_grading
from contentstore.utils import update_item
from xmodule.fields import Date
import re
import logging
import datetime
from xmodule.modulestore.django import loc_mapper
class CourseDetails(object):
def __init__(self, location):
self.course_location = location # a Location obj
def __init__(self, org, course_id, run):
# still need these for now b/c the client's screen shows these 3 fields
self.org = org
self.course_id = course_id
self.run = run
self.start_date = None # 'start'
self.end_date = None # 'end'
self.enrollment_start = None
......@@ -27,16 +32,13 @@ class CourseDetails(object):
self.course_image_asset_path = "" # URL of the course image
@classmethod
def fetch(cls, course_location):
def fetch(cls, course_locator):
"""
Fetch the course details for the given course from persistence and return a CourseDetails model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = cls(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
course = cls(course_old_location.org, course_old_location.course, course_old_location.name)
course.start_date = descriptor.start
course.end_date = descriptor.end
......@@ -45,7 +47,7 @@ class CourseDetails(object):
course.course_image_name = descriptor.course_image
course.course_image_asset_path = course_image_url(descriptor)
temploc = course_location.replace(category='about', name='syllabus')
temploc = course_old_location.replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
......@@ -73,14 +75,12 @@ class CourseDetails(object):
return course
@classmethod
def update_from_json(cls, jsondict):
def update_from_json(cls, course_locator, jsondict):
"""
Decode the json into CourseDetails and save any changed attrs to the db
"""
# TODO make it an error for this to be undefined & for it to not be retrievable from modulestore
course_location = Location(jsondict['course_location'])
# Will probably want to cache the inflight courses because every blur generates an update
descriptor = get_modulestore(course_location).get_item(course_location)
course_old_location = loc_mapper().translate_locator_to_location(course_locator)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
dirty = False
......@@ -134,11 +134,11 @@ class CourseDetails(object):
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
get_modulestore(course_old_location).update_metadata(course_old_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
temploc = Location(course_location).replace(category='about', name='syllabus')
temploc = Location(course_old_location).replace(category='about', name='syllabus')
update_item(temploc, jsondict['syllabus'])
temploc = temploc.replace(name='overview')
......@@ -151,9 +151,9 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return CourseDetails.fetch(course_location)
return CourseDetails.fetch(course_locator)
@staticmethod
def parse_video_tag(raw_video):
......@@ -188,6 +188,9 @@ class CourseDetails(object):
# TODO move to a more general util?
class CourseSettingsEncoder(json.JSONEncoder):
"""
Serialize CourseDetails, CourseGradingModel, datetime, and old Locations
"""
def default(self, obj):
if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
return obj.__dict__
......
from xmodule.modulestore import Location
from xblock.fields import Scope
from contentstore.utils import get_modulestore
from xmodule.modulestore.inheritance import own_metadata
from xblock.fields import Scope
from cms.xmodule_namespace import CmsBlockMixin
......@@ -20,21 +20,18 @@ class CourseMetadata(object):
'tabs',
'graceperiod',
'checklists',
'show_timezone'
'show_timezone',
'format',
'graded',
]
@classmethod
def fetch(cls, course_location):
def fetch(cls, descriptor):
"""
Fetch the key:value editable course details for the given course from
persistence and return a CourseMetadata model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = {}
descriptor = get_modulestore(course_location).get_item(course_location)
result = {}
for field in descriptor.fields.values():
if field.name in CmsBlockMixin.fields:
......@@ -46,19 +43,17 @@ class CourseMetadata(object):
if field.name in cls.FILTERED_LIST:
continue
course[field.name] = field.read_json(descriptor)
result[field.name] = field.read_json(descriptor)
return course
return result
@classmethod
def update_from_json(cls, course_location, jsondict, filter_tabs=True):
def update_from_json(cls, descriptor, jsondict, filter_tabs=True):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
# Copy the filtered list to avoid permanently changing the class attribute.
......@@ -72,39 +67,17 @@ class CourseMetadata(object):
if key in filtered_list:
continue
if key == "unsetKeys":
dirty = True
for unset in val:
descriptor.fields[unset].delete_from(descriptor)
if hasattr(descriptor, key) and getattr(descriptor, key) != val:
dirty = True
value = descriptor.fields[key].from_json(val)
setattr(descriptor, key, value)
if dirty:
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads,
# but I put the reads in as a means to confirm it persisted correctly
return cls.fetch(course_location)
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a
single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if hasattr(descriptor, key):
delattr(descriptor, key)
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
descriptor.save()
get_modulestore(course_location).update_metadata(course_location,
own_metadata(descriptor))
get_modulestore(descriptor.location).update_metadata(descriptor.location, own_metadata(descriptor))
return cls.fetch(course_location)
return cls.fetch(descriptor)
......@@ -34,7 +34,7 @@ DOC_STORE_CONFIG = {
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
'render_template': 'edxmako.shortcuts.render_to_string',
}
MODULESTORE = {
......@@ -87,7 +87,7 @@ PIPELINE = True
STATICFILES_FINDERS += ('pipeline.finders.PipelineFinder', )
# Use the auto_auth workflow for creating users and logging them in
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files
......
......@@ -90,7 +90,10 @@ with open(CONFIG_ROOT / CONFIG_PREFIX + "env.json") as env_file:
STATIC_URL_BASE = ENV_TOKENS.get('STATIC_URL_BASE', None)
if STATIC_URL_BASE:
# collectstatic will fail if STATIC_URL is a unicode string
STATIC_URL = STATIC_URL_BASE.encode('ascii') + "/" + git.revision + "/"
STATIC_URL = STATIC_URL_BASE.encode('ascii')
if not STATIC_URL.endswith("/"):
STATIC_URL += "/"
STATIC_URL += git.revision + "/"
# GITHUB_REPO_ROOT is the base directory
# for course data
......@@ -106,7 +109,7 @@ if STATIC_ROOT_BASE:
EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND)
EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None)
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
# Note that MITX_FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.
# Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.
SITE_NAME = ENV_TOKENS['SITE_NAME']
......@@ -138,8 +141,9 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value
ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {}))
for feature, value in ENV_FEATURES.items():
FEATURES[feature] = value
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
......@@ -164,11 +168,16 @@ with open(CONFIG_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
# Note that this is the Studio key. There is a separate key for the LMS.
SEGMENT_IO_KEY = AUTH_TOKENS.get('SEGMENT_IO_KEY')
if SEGMENT_IO_KEY:
MITX_FEATURES['SEGMENT_IO'] = ENV_TOKENS.get('SEGMENT_IO', False)
FEATURES['SEGMENT_IO'] = ENV_TOKENS.get('SEGMENT_IO', False)
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
if AWS_ACCESS_KEY_ID == "":
AWS_ACCESS_KEY_ID = None
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
if AWS_SECRET_ACCESS_KEY == "":
AWS_SECRET_ACCESS_KEY = None
DATABASES = AUTH_TOKENS['DATABASES']
MODULESTORE = AUTH_TOKENS['MODULESTORE']
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
......
......@@ -2,7 +2,7 @@
This is the common settings file, intended to set sane defaults. If you have a
piece of configuration that's dependent on a set of feature flags being set,
then create a function that returns the calculated value based on the value of
MITX_FEATURES[...]. Modules that extend this one can change the feature
FEATURES[...]. Modules that extend this one can change the feature
configuration in an environment specific config file and re-calculate those
values.
......@@ -14,7 +14,7 @@ Longer TODO:
1. Right now our treatment of static content in general and in particular
course-specific static content is haphazard.
2. We should have a more disciplined approach to feature flagging, even if it
just means that we stick them in a dict called MITX_FEATURES.
just means that we stick them in a dict called FEATURES.
3. We need to handle configuration for multiple courses. This could be as
multiple sites, but we do need a way to map their data assets.
"""
......@@ -36,7 +36,7 @@ from dealer.git import git
############################ FEATURE CONFIGURATION #############################
MITX_FEATURES = {
FEATURES = {
'USE_DJANGO_PIPELINE': True,
'GITHUB_PUSH': False,
......@@ -99,10 +99,10 @@ for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems():
TEMPLATE_DIRS = MAKO_TEMPLATES['main']
MITX_ROOT_URL = ''
EDX_ROOT_URL = ''
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/signin'
LOGIN_URL = MITX_ROOT_URL + '/signin'
LOGIN_REDIRECT_URL = EDX_ROOT_URL + '/signin'
LOGIN_URL = EDX_ROOT_URL + '/signin'
TEMPLATE_CONTEXT_PROCESSORS = (
......@@ -161,7 +161,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware',
'edxmako.middleware.MakoMiddleware',
# Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware',
......@@ -393,7 +393,7 @@ INSTALLED_APPS = (
'datadog',
# For asset pipelining
'mitxmako',
'edxmako',
'pipeline',
'staticfiles',
'static_replace',
......@@ -414,14 +414,7 @@ INSTALLED_APPS = (
EDXMKTG_COOKIE_NAME = 'edxloggedin'
MKTG_URLS = {}
MKTG_URL_LINK_MAP = {
'ABOUT': 'about_edx',
'CONTACT': 'contact',
'FAQ': 'help_edx',
'COURSES': 'courses',
'ROOT': 'root',
'TOS': 'tos',
'HONOR': 'honor',
'PRIVACY': 'privacy_edx',
}
COURSES_WITH_UNSAFE_CODE = []
......
......@@ -30,7 +30,7 @@ DOC_STORE_CONFIG = {
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
'render_template': 'edxmako.shortcuts.render_to_string',
}
MODULESTORE = {
......@@ -76,7 +76,7 @@ DATABASES = {
}
LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "localhost:8000"
REPOS = {
'edx4edx': {
......@@ -178,10 +178,10 @@ DEBUG_TOOLBAR_CONFIG = {
DEBUG_TOOLBAR_MONGO_STACKTRACES = False
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
FEATURES['STUDIO_NPS_SURVEY'] = False
# Enable URL that shows information about the status of variuous services
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_SERVICE_STATUS'] = True
############################# SEGMENT-IO ##################################
......@@ -190,7 +190,7 @@ MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
import os
SEGMENT_IO_KEY = os.environ.get('SEGMENT_IO_KEY')
if SEGMENT_IO_KEY:
MITX_FEATURES['SEGMENT_IO'] = True
FEATURES['SEGMENT_IO'] = True
#####################################################################
......
......@@ -9,8 +9,8 @@
from .common import *
from .dev import *
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
MITX_FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss
FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
......@@ -9,4 +9,4 @@ the same process between preview and published
from .dev import *
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost:8000"
......@@ -23,7 +23,8 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
################################# LMS INTEGRATION #############################
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost:8000"
LMS_BASE = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE
################################# CELERY ######################################
......
......@@ -60,7 +60,7 @@ DOC_STORE_CONFIG = {
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
'render_template': 'edxmako.shortcuts.render_to_string',
}
MODULESTORE = {
......@@ -109,7 +109,7 @@ DATABASES = {
}
LMS_BASE = "localhost:8000"
MITX_FEATURES['PREVIEW_LMS_BASE'] = "preview"
FEATURES['PREVIEW_LMS_BASE'] = "preview"
CACHES = {
# This is the cache used for most things. Askbot will not work without a
......@@ -161,9 +161,9 @@ PASSWORD_HASHERS = (
SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
FEATURES['STUDIO_NPS_SURVEY'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_SERVICE_STATUS'] = True
# This is to disable a test under the common directory that will not pass when run under CMS
MITX_FEATURES['DISABLE_PASSWORD_RESET_EMAIL_TEST'] = True
FEATURES['DISABLE_PASSWORD_RESET_EMAIL_TEST'] = True
......@@ -197,7 +197,8 @@ define([
"js/spec/transcripts/videolist_spec", "js/spec/transcripts/message_manager_spec",
"js/spec/transcripts/file_uploader_spec",
"js/spec/utils/module_spec"
"js/spec/utils/module_spec",
"js/spec/models/explicit_url_spec"
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
......
......@@ -196,3 +196,22 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@handoutsEdit.$el.find('.edit-button').click()
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
it "can open course handouts with bad html on edit", ->
# Enter some bad html in handouts section, verifying that the
# model/handoutform opens when "Edit" is clicked
@model = new ModuleInfo({
id: 'handouts-id',
data: '<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>'
})
@handoutsEdit = new CourseInfoHandoutsView({
el: $('#course-handouts-view'),
model: @model,
base_asset_url: 'base-asset-url/'
});
@handoutsEdit.render()
expect($('.edit-handouts-form').is(':hidden')).toEqual(true)
@handoutsEdit.$el.find('.edit-button').click()
expect(@handoutsEdit.$codeMirror.getValue()).toEqual('<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>')
expect($('.edit-handouts-form').is(':hidden')).toEqual(false)
\ No newline at end of file
define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (ModuleEdit, ModuleModel) ->
describe "ModuleEdit", ->
beforeEach ->
@stubModule = jasmine.createSpy("Module")
@stubModule.id = 'stub-id'
@stubModule.get = (param)->
if param == 'old_id'
return 'stub-old-id'
@stubModule = new ModuleModel
id: "stub-id"
setFixtures """
<li class="component" id="stub-id">
......@@ -62,7 +59,7 @@ define ["coffee/src/views/module_edit", "xmodule"], (ModuleEdit) ->
@moduleEdit.render()
it "loads the module preview and editor via ajax on the view element", ->
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/preview_component/#{@moduleEdit.model.get('old_id')}", jasmine.any(Function))
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/xblock/#{@moduleEdit.model.id}", jasmine.any(Function))
@moduleEdit.$el.load.mostRecentCall.args[1]()
expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
......
......@@ -36,7 +36,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
appendSetFixtures """
<section class="courseware-section branch" data-locator="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here" data-locator="an-id-goes-here">
<li class="branch collapsed id-holder" data-locator="an-id-goes-here">
<a href="#" class="delete-section-button"></a>
</li>
</section>
......
......@@ -69,15 +69,13 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
payload
(data) =>
@model.set(id: data.locator)
@model.set(old_id: data.id)
@$el.data('id', data.id)
@$el.data('locator', data.locator)
@render()
)
render: ->
if @model.get('old_id')
@$el.load("/preview_component/#{@model.get('old_id')}", =>
if @model.id
@$el.load(@model.url(), =>
@loadDisplay()
@delegateEvents()
)
......
......@@ -6,8 +6,7 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
initialize: =>
@$('.component').each((idx, element) =>
model = new ModuleModel({
id: $(element).data('locator'),
old_id:$(element).data('id')
id: $(element).data('locator')
})
new ModuleEditView(
......@@ -38,14 +37,17 @@ define ["jquery", "jquery.ui", "backbone", "js/views/feedback_prompt", "js/views
analytics.track "Reordered Static Pages",
course: course_location_analytics
saving = new NotificationView.Mini({title: gettext("Saving&hellip;")})
saving.show()
$.ajax({
type:'POST',
url: '/reorder_static_tabs',
url: @model.url(),
data: JSON.stringify({
tabs : tabs
}),
contentType: 'application/json'
})
}).success(=> saving.hide())
addNewTab: (event) =>
event.preventDefault()
......
......@@ -63,7 +63,6 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@$('.component').each (idx, element) =>
model = new ModuleModel
id: $(element).data('locator')
old_id: $(element).data('id')
new ModuleEditView
el: element,
onDelete: @deleteComponent,
......@@ -167,7 +166,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true)
$.ajax({
type: 'DELETE',
url: @model.urlRoot + "/" + @$el.data('locator') + "?" + $.param({recurse: true})
url: @model.url() + "?" + $.param({recurse: true})
}).success(=>
analytics.track "Deleted Draft",
......@@ -180,8 +179,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
createDraft: (event) ->
@wait(true)
$.postJSON('/create_draft', {
id: @$el.data('id')
$.postJSON(@model.url(), {
publish: 'create_draft'
}, =>
analytics.track "Created Draft",
course: course_location_analytics
......@@ -194,8 +193,8 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@wait(true)
@saveDraft()
$.postJSON('/publish_draft', {
id: @$el.data('id')
$.postJSON(@model.url(), {
publish: 'make_public'
}, =>
analytics.track "Published Draft",
course: course_location_analytics
......@@ -206,16 +205,16 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
setVisibility: (event) ->
if @$('.visibility-select').val() == 'private'
target_url = '/unpublish_unit'
action = 'make_private'
visibility = "private"
else
target_url = '/publish_draft'
action = 'make_public'
visibility = "public"
@wait(true)
$.postJSON(target_url, {
id: @$el.data('id')
$.postJSON(@model.url(), {
publish: action
}, =>
analytics.track "Set Unit Visibility",
course: course_location_analytics
......
......@@ -237,7 +237,7 @@ function createNewUnit(e) {
function(data) {
// redirect to the edit page
window.location = "/edit/" + data['id'];
window.location = "/unit/" + data['locator'];
});
}
......
......@@ -2,10 +2,6 @@ define(["backbone", "js/models/settings/course_grader"], function(Backbone, Cour
var CourseGraderCollection = Backbone.Collection.extend({
model : CourseGrader,
course_location : null, // must be set to a Location object
url : function() {
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/';
},
sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
}
......
define(["backbone", "underscore", "js/models/location"], function(Backbone, _, Location) {
define(["backbone", "underscore"], function(Backbone, _) {
var AssignmentGrade = Backbone.Model.extend({
defaults : {
graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral
location : null // A location object
graderType : null, // the type label (string). May be "Not Graded" which implies None.
locator : null // locator for the block
},
initialize : function(attrs) {
if (attrs['assignmentUrl']) {
this.set('location', new Location(attrs['assignmentUrl'], {parse: true}));
}
},
parse : function(attrs) {
if (attrs && attrs['location']) {
attrs.location = new Location(attrs['location'], {parse: true});
}
},
urlRoot : function() {
if (this.has('location')) {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/';
}
else return "";
idAttribute: 'locator',
urlRoot : '/xblock/',
url: function() {
// add ?fields=graderType to the request url (only needed for fetch, but innocuous for others)
return Backbone.Model.prototype.url.apply(this) + '?' + $.param({fields: 'graderType'});
}
});
return AssignmentGrade;
......
......@@ -5,12 +5,9 @@ define(["backbone"], function(Backbone) {
url: '',
defaults: {
"courseId": "", // the location url
"updates" : null, // UpdateCollection
"handouts": null // HandoutCollection
},
idAttribute : "courseId"
}
});
return CourseInfo;
});
/**
* A model that simply allows the update URL to be passed
* in as an argument.
*/
define(["backbone"], function(Backbone){
return Backbone.Model.extend({
defaults: {
"explicit_url": ""
},
url: function() {
return this.get("explicit_url");
}
});
});
define(["backbone", "underscore", "gettext", "js/models/location"], function(Backbone, _, gettext, Location) {
define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
var CourseDetails = Backbone.Model.extend({
defaults: {
location : null, // the course's Location model, required
org : '',
course_id: '',
run: '',
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
enrollment_start: null,
......@@ -17,9 +19,6 @@ var CourseDetails = Backbone.Model.extend({
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
parse: function(attributes) {
if (attributes['course_location']) {
attributes.location = new Location(attributes.course_location, {parse:true});
}
if (attributes['start_date']) {
attributes.start_date = new Date(attributes.start_date);
}
......
......@@ -3,15 +3,11 @@ define(["backbone", "js/models/location", "js/collections/course_grader"],
var CourseGradingPolicy = Backbone.Model.extend({
defaults : {
course_location : null,
graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...}
},
parse: function(attributes) {
if (attributes['course_location']) {
attributes.course_location = new Location(attributes.course_location, {parse:true});
}
if (attributes['graders']) {
var graderCollection;
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created
......@@ -21,7 +17,6 @@ var CourseGradingPolicy = Backbone.Model.extend({
}
else {
graderCollection = new CourseGraderCollection(attributes.graders, {parse:true});
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
}
attributes.graders = graderCollection;
}
......@@ -35,10 +30,6 @@ var CourseGradingPolicy = Backbone.Model.extend({
}
return attributes;
},
url : function() {
var location = this.get('course_location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/grading';
},
gracePeriodToDate : function() {
var newDate = new Date();
if (this.has('grace_period') && this.get('grace_period')['hours'])
......
define(['js/models/explicit_url'],
function (Model) {
describe('Model ', function () {
it('allows url to be passed in constructor', function () {
expect(new Model({'explicit_url': '/fancy/url'}).url()).toBe('/fancy/url');
});
it('returns empty string if url not set', function () {
expect(new Model().url()).toBe('');
});
});
}
);
......@@ -48,7 +48,7 @@ function ($, _, Utils, FileUploader) {
el: $container,
messenger: messenger,
videoListObject: videoListObject,
component_id: 'component_id'
component_locator: 'component_locator'
});
});
......
......@@ -52,7 +52,7 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
view = new MessageManager({
el: $container,
parent: videoList,
component_id: 'component_id'
component_locator: 'component_locator'
});
});
......@@ -60,7 +60,7 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
expect(fileUploader.initialize).toHaveBeenCalledWith({
el: view.$el,
messenger: view,
component_id: view.component_id,
component_locator: view.component_locator,
videoListObject: view.options.parent
});
});
......@@ -215,7 +215,7 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
function() {
expect(Utils.command).toHaveBeenCalledWith(
action,
view.component_id,
view.component_locator,
videoList,
void(0)
);
......@@ -245,7 +245,7 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
function () {
expect(Utils.command).toHaveBeenCalledWith(
action,
view.component_id,
view.component_locator,
videoList,
{
html5_id: extraParamas
......@@ -268,7 +268,7 @@ function ($, _, Utils, MessageManager, FileUploader, sinon) {
function () {
expect(Utils.command).toHaveBeenCalledWith(
action,
view.component_id,
view.component_locator,
videoList,
void(0)
);
......
......@@ -11,7 +11,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
'transcripts/metadata-videolist-entry.underscore'
),
abstractEditor = AbstractEditor.prototype,
component_id = 'component_id',
component_locator = 'component_locator',
videoList = [
{
mode: "youtube",
......@@ -62,7 +62,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
var tpl = sandbox({
'class': 'component',
'data-id': component_id
'data-locator': component_locator
}),
model = new MetadataModel(modelStub),
videoList, $el;
......@@ -157,7 +157,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
waitsForResponse(function () {
expect(abstractEditor.initialize).toHaveBeenCalled();
expect(messenger.initialize).toHaveBeenCalled();
expect(view.component_id).toBe(component_id);
expect(view.component_locator).toBe(component_locator);
expect(view.$el).toHandle('input');
});
});
......@@ -167,7 +167,7 @@ function ($, _, Utils, VideoList, MetadataView, MetadataModel, AbstractEditor, s
expect(abstractEditor.render).toHaveBeenCalled();
expect(Utils.command).toHaveBeenCalledWith(
'check',
component_id,
component_locator,
videoList
);
......
......@@ -30,6 +30,7 @@ define(["backbone", "underscore", "codemirror", "js/views/feedback_notification"
model: this.model
}))
);
$('.handouts-content').html(this.model.get('data'));
this.$preview = this.$el.find('.handouts-content');
this.$form = this.$el.find(".edit-handouts-form");
this.$editor = this.$form.find('.handouts-content-editor');
......@@ -50,32 +51,43 @@ define(["backbone", "underscore", "codemirror", "js/views/feedback_notification"
},
onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue());
var saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
saving.show();
this.model.save({}, {
success: function() {
saving.hide();
}
});
this.render();
this.$form.hide();
this.closeEditor();
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
});
$('#handout_error').removeClass('is-shown');
$('.save-button').removeClass('is-disabled');
if ($('.CodeMirror-lines').find('.cm-error').length == 0){
this.model.set('data', this.$codeMirror.getValue());
var saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
saving.show();
this.model.save({}, {
success: function() {
saving.hide();
}
});
this.render();
this.$form.hide();
this.closeEditor();
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
});
}else{
$('#handout_error').addClass('is-shown');
$('.save-button').addClass('is-disabled');
event.preventDefault();
}
},
onCancel: function(event) {
$('#handout_error').removeClass('is-shown');
$('.save-button').removeClass('is-disabled');
this.$form.hide();
this.closeEditor();
},
closeEditor: function() {
$('#handout_error').removeClass('is-shown');
$('.save-button').removeClass('is-disabled');
this.$form.hide();
ModalUtils.hideModalCover();
this.$form.find('.CodeMirror').remove();
......
......@@ -6,7 +6,10 @@ define(["codemirror", "utility"],
var $codeMirror = CodeMirror.fromTextArea(textArea, {
mode: "text/html",
lineNumbers: true,
lineWrapping: true
lineWrapping: true,
onChange: function () {
$('.save-button').removeClass('is-disabled');
}
});
$codeMirror.setValue(content);
$codeMirror.clearHistory();
......
......@@ -21,7 +21,7 @@ define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/v
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>');
this.assignmentGrade = new AssignmentGrade({
assignmentUrl : this.$el.closest('.id-holder').data('id'),
locator : this.$el.closest('.id-holder').data('locator'),
graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null
this.graders = this.options['graders'];
......
......@@ -21,9 +21,9 @@ var DetailsView = ValidatingView.extend({
initialize : function() {
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>');
// fill in fields
this.$el.find("#course-name").val(this.model.get('location').get('name'));
this.$el.find("#course-organization").val(this.model.get('location').get('org'));
this.$el.find("#course-number").val(this.model.get('location').get('course'));
this.$el.find("#course-organization").val(this.model.get('org'));
this.$el.find("#course-number").val(this.model.get('course_id'));
this.$el.find("#course-name").val(this.model.get('run'));
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
// Avoid showing broken image on mistyped/nonexistent image
......
......@@ -72,7 +72,7 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
syncBasicTab: function (metadataCollection, metadataView) {
var result = [],
getField = Utils.getField,
component_id = this.$el.closest('.component').data('id'),
component_locator = this.$el.closest('.component').data('locator'),
subs = getField(metadataCollection, 'sub'),
values = {},
videoUrl, metadata, modifiedValues;
......@@ -90,7 +90,17 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
var isSubsModified = (function (values) {
var isSubsChanged = subs.hasChanged("value");
return Boolean(isSubsChanged && _.isString(values.sub));
return Boolean(
isSubsChanged &&
(
// If the user changes the field, `values.sub` contains
// string value;
// If the user clicks `clear` button, the field contains
// null value.
// Otherwise, undefined.
_.isString(values.sub) || _.isNull(subs.getValue())
)
);
}(modifiedValues));
// When we change value of `sub` field in the `Advanced`,
......@@ -99,7 +109,7 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
if (isSubsModified) {
metadata = $.extend(true, {}, modifiedValues);
// Save module state
Utils.command('save', component_id, null, {
Utils.command('save', component_locator, null, {
metadata: metadata,
current_subs: _.pluck(
Utils.getVideoList(videoUrl.getDisplayValue()),
......@@ -110,18 +120,16 @@ function($, Backbone, _, Utils, MetadataView, MetadataCollection) {
// Get values from `Advanced` tab fields (`html5_sources`,
// `youtube_id_1_0`) that should be synchronized.
html5Sources = getField(metadataCollection, 'html5_sources')
                                    .getDisplayValue();
var html5Sources = getField(metadataCollection, 'html5_sources').getDisplayValue();
            values.youtube = getField(metadataCollection, 'youtube_id_1_0')
                                    .getDisplayValue();
values.youtube = getField(metadataCollection, 'youtube_id_1_0').getDisplayValue();
            values.html5Sources = _.filter(html5Sources, function (value) {
                var link = Utils.parseLink(value),
values.html5Sources = _.filter(html5Sources, function (value) {
var link = Utils.parseLink(value),
mode = link && link.mode;
                return mode === 'html5' && mode;
            });
return mode === 'html5' && mode;
});
// The length of youtube video_id should be 11 characters.
......
......@@ -39,7 +39,7 @@ function($, Backbone, _, Utils) {
tplContainer.html(this.template({
ext: this.validFileExtensions,
component_id: this.options.component_id,
component_locator: this.options.component_locator,
video_list: videoList
}));
......
......@@ -31,12 +31,12 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
initialize: function () {
_.bindAll(this);
this.component_id = this.$el.closest('.component').data('id');
this.component_locator = this.$el.closest('.component').data('locator');
this.fileUploader = new FileUploader({
el: this.$el,
messenger: this,
component_id: this.component_id,
component_locator: this.component_locator,
videoListObject: this.options.parent
});
},
......@@ -76,7 +76,7 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
this.$el.find('.transcripts-status')
.removeClass('is-invisible')
.find(this.elClass).html(template({
component_id: encodeURIComponent(this.component_id),
component_locator: encodeURIComponent(this.component_locator),
html5_list: html5List,
grouped_list: groupedList,
subs_id: (params) ? params.subs: ''
......@@ -204,7 +204,7 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
*/
processCommand: function (action, errorMessage, videoId) {
var self = this,
component_id = this.component_id,
component_locator = this.component_locator,
videoList = this.options.parent.getVideoObjectsList(),
extraParam, xhr;
......@@ -212,7 +212,7 @@ function($, Backbone, _, Utils, FileUploader, gettext) {
extraParam = { html5_id: videoId };
}
xhr = Utils.command(action, component_id, videoList, extraParam)
xhr = Utils.command(action, component_locator, videoList, extraParam)
.done(function (resp) {
var sub = resp.subs;
......
......@@ -46,7 +46,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
_.debounce(_.bind(this.inputHandler, this), this.inputDelay)
);
this.component_id = this.$el.closest('.component').data('id');
this.component_locator = this.$el.closest('.component').data('locator');
},
render: function () {
......@@ -55,7 +55,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
.apply(this, arguments);
var self = this,
component_id = this.$el.closest('.component').data('id'),
component_locator = this.$el.closest('.component').data('locator'),
videoList = this.getVideoObjectsList(),
showServerError = function (response) {
......@@ -82,7 +82,7 @@ function($, Backbone, _, AbstractEditor, Utils, MessageManager, MetadataView) {
}
// Check current state of Timed Transcripts.
Utils.command('check', component_id, videoList)
Utils.command('check', component_locator, videoList)
.done(function (resp) {
var params = resp,
len = videoList.length,
......
......@@ -295,7 +295,7 @@ define(["jquery", "underscore", "jquery.ajaxQueue"], function($, _) {
*
* @param {string} action Action that will be invoked on server. Is a part
* of url.
* @param {string} component_id Id of component.
* @param {string} component_locator the locator of component.
* @param {array} videoList List of object with information about inserted
* urls.
* @param {object} extraParams Extra parameters that can be send to the
......@@ -314,7 +314,7 @@ define(["jquery", "underscore", "jquery.ajaxQueue"], function($, _) {
// _command() function.
var xhr = null;
return function (action, component_id, videoList, extraParams) {
return function (action, component_locator, videoList, extraParams) {
var params, data;
if (extraParams) {
......@@ -326,7 +326,7 @@ define(["jquery", "underscore", "jquery.ajaxQueue"], function($, _) {
}
data = $.extend(
{ id: component_id },
{ locator: component_locator },
{ videos: videoList },
params
);
......
......@@ -172,6 +172,15 @@ $tmg-f3: 0.125s;
// ====================
// archetype UI
$ui-action-primary-color: $blue-u2;
$ui-action-primary-color-focus: $blue-s1;
$ui-link-color: $blue-u2;
$ui-link-color-focus: $blue-s1;
// ====================
// specific UI
$ui-notification-height: ($baseline*10);
$ui-update-color: $blue-l4;
......
......@@ -10,6 +10,10 @@
&.is-shown {
bottom: 0;
}
&.is-hiding {
bottom: -($ui-notification-height);
}
}
}
......
......@@ -23,10 +23,10 @@
}
a {
color: $gray;
color: $ui-link-color;
&:hover, &:active {
color: $gray-d2;
color: $ui-link-color-focus;
}
}
......@@ -37,7 +37,7 @@
.nav-item {
display: inline-block;
margin-right: ($baseline/2);
margin-right: ($baseline/4);
&:last-child {
margin-right: 0;
......@@ -45,7 +45,7 @@
a {
border-radius: 2px;
padding: ($baseline/2) ($baseline*0.75);
padding: ($baseline/2) ($baseline/2);
background: transparent;
[class^="icon-"] {
......@@ -54,19 +54,6 @@
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
color: $gray-l1;
}
&:hover, &:active {
color: $gray-d2;
[class^="icon-"] {
color: $gray-d2;
}
}
&.is-active {
color: $gray-d2;
}
}
}
......
// studio - elements - xmodules
// studio - elements - xmodules & xblocks
// ====================
// general - display mode (xblock-student_view or xmodule_display)
.xmodule_display, .xblock-student_view {
// font styling
i, em {
font-style: italic;
}
}
// ====================
// Video Alpha
......
......@@ -187,7 +187,7 @@ require(["domReady", "jquery", "gettext", "js/models/asset", "js/collections/ass
<a href="#" class="close-button"><i class="icon-remove-sign"></i> <span class="sr">${_('close')}</span></a>
<div class="modal-body">
<h1 class="title">${_("Upload New File")}</h1>
<p class="file-name"></a>
<p class="file-name">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
......
......@@ -20,7 +20,7 @@
</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<meta name="path_prefix" content="${EDX_ROOT_URL}">
<%static:css group='style-vendor'/>
<%static:css group='style-app'/>
......
......@@ -33,7 +33,6 @@ require(["domReady!", "jquery", "js/collections/course_update", "js/models/modul
var editor = new CourseInfoEditView({
el: $('.main-wrapper'),
model : new CourseInfoModel({
courseId : '${context_course.location}',
updates : course_updates,
base_asset_url : '${base_asset_url}',
handouts : course_handouts
......
......@@ -9,12 +9,15 @@
<%block name="jsextra">
<script type='text/javascript'>
require(["backbone", "coffee/src/views/tabs"], function(Backbone, TabsEditView) {
require(["js/models/explicit_url", "coffee/src/views/tabs"], function(TabsModel, TabsEditView) {
var model = new TabsModel({
id: "${course_locator}",
explicit_url: "${course_locator.url_reverse('tabs')}"
});
new TabsEditView({
el: $('.main-wrapper'),
model: new Backbone.Model({
id: '${locator}'
}),
model: model,
mast: $('.wrapper-mast')
});
});
......@@ -61,8 +64,8 @@ require(["backbone", "coffee/src/views/tabs"], function(Backbone, TabsEditView)
<div class="tab-list">
<ol class='components'>
% for id, locator in components:
<li class="component" data-id="${id}" data-locator="${locator}"/>
% for locator in components:
<li class="component" data-locator="${locator}"/>
% endfor
<li class="new-component-item">
......
......@@ -31,7 +31,7 @@
</div>
<div class="sidebar">
<div class="unit-settings window id-holder" data-id="${subsection.location}">
<div class="unit-settings window id-holder" data-locator="${locator}">
<h4 class="header">${_("Subsection Settings")}</h4>
<div class="window-contents">
<div class="scheduled-date-input row">
......@@ -115,7 +115,6 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
}
$(".gradable-status").each(function(index, ele) {
......
......@@ -45,8 +45,8 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
% if course_creator_status=='granted':
<a href="#" class="button new-button new-course-button"><i class="icon-plus icon-inline"></i>
${_("New Course")}</a>
% elif course_creator_status=='disallowed_for_this_site' and settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL',''):
<a href="mailto:${settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("Email staff to create course")}</a>
% elif course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):
<a href="mailto:${settings.FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("Email staff to create course")}</a>
% endif
</li>
</ul>
......@@ -290,10 +290,10 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
</ol>
</div>
% if course_creator_status=='disallowed_for_this_site' and settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL',''):
% if course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):
<div class="bit">
<h3 class="title title-3">${_('Can I create courses in Studio?')}</h3>
<p>${_('In order to create courses in Studio, you must')} <a href="mailto:${settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("contact edX staff to help you create a course")}</a></p>
<p>${_('In order to create courses in Studio, you must')} <a href="mailto:${settings.FEATURES.get('STUDIO_REQUEST_EMAIL','')}">${_("contact edX staff to help you create a course")}</a></p>
</div>
% endif
......
......@@ -3,12 +3,13 @@
<h2 class="title">Course Handouts</h2>
<%if (model.get('data') != null) { %>
<div class="handouts-content">
<%= model.get('data') %>
</div>
<% } else {%>
<p>${_("You have no handouts defined")}</p>
<% } %>
<form class="edit-handouts-form" style="display: block;">
<div class="message message-status error" name="handout_html_error" id="handout_error"><%=gettext("There is invalid code in your content. Please check to make sure it is valid HTML.")%></div>
<div class="row">
<textarea class="handouts-content-editor text-editor"></textarea>
</div>
......
......@@ -5,6 +5,6 @@
method="post" enctype="multipart/form-data">
<input type="file" class="file-input" name="file"
accept="<%= _.map(ext, function(val){ return '.' + val; }).join(', ') %>">
<input type="hidden" name="id" value="<%= component_id %>">
<input type="hidden" name="locator" value="<%= component_locator %>">
<input type="hidden" name="video_list" value='<%= JSON.stringify(video_list) %>'>
</form>
......@@ -10,7 +10,7 @@
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Timed Transcript") %>" data-tooltip="<%= gettext("Upload New Timed Transcript") %>">
<span><%= gettext("Upload New Timed Transcript") %></span>
</button>
<a class="action setting-download" href="/transcripts/download?id=<%= component_id %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download to Edit") %>">
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download to Edit") %>">
<span><%= gettext("Download to Edit") %></span>
</a>
</div>
......@@ -10,7 +10,7 @@
<button class="action setting-upload" type="button" name="setting-upload" value="<%= gettext("Upload New Timed Transcript") %>" data-tooltip="<%= gettext("Upload New Timed Transcript") %>">
<span><%= gettext("Upload New Timed Transcript") %></span>
</button>
<a class="action setting-download" href="/transcripts/download?id=<%= component_id %>" data-tooltip="<%= gettext("Download to Edit") %>">
<a class="action setting-download" href="/transcripts/download?locator=<%= component_locator %>&subs_id=<%= subs_id %>" data-tooltip="<%= gettext("Download to Edit") %>">
<span><%= gettext("Download to Edit") %></span>
</a>
</div>
......@@ -27,7 +27,6 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
// but we really should change that behavior.
if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
}
$(".gradable-status").each(function(index, ele) {
......@@ -200,7 +199,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
context_course.location.course_id, subsection.location, False, True
)
%>
<li class="courseware-subsection branch collapsed id-holder is-draggable" data-id="${subsection.location}"
<li class="courseware-subsection branch collapsed id-holder is-draggable"
data-parent="${section_locator}" data-locator="${subsection_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" />
......@@ -208,7 +207,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="section-item">
<div class="details">
<a href="#" data-tooltip="${_('Expand/collapse this subsection')}" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<a href="${subsection_locator.url_reverse('subsection')}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a>
......
......@@ -4,10 +4,8 @@
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
from django.core.urlresolvers import reverse
%>
<%block name="jsextra">
......@@ -69,17 +67,20 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">${_("Organization")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="long" id="course-organization" readonly />
</li>
<li class="field text is-not-editable" id="field-course-number">
<label for="course-number">${_("Course Number")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="short" id="course-number" readonly>
</li>
<li class="field text is-not-editable" id="field-course-name">
<label for="course-name">${_("Course Name")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly />
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="long" id="course-name" readonly />
</li>
</ol>
......@@ -87,12 +88,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
<div class="copy">
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
<p><a class="link-courseURL" rel="external" href="https:${lms_link_for_about_page}">https:${lms_link_for_about_page}</a></p>
</div>
<ul class="list-actions">
<li class="action-item">
<a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
<a title="${_('Send a note to students via email')}"
href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${lms_link_for_about_page}%20to%20enroll." class="action action-primary">
<i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
</li>
</ul>
</div>
......@@ -199,7 +202,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<%def name='overview_text()'><%
a_link_start = '<a class="link-courseURL" rel="external" href="'
a_link_end = '">' + _("your course summary page") + '</a>'
a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end
a_link = a_link_start + lms_link_for_about_page + a_link_end
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
%>${text}</%def>
<span class="tip tip-stacked">${overview_text()}</span>
......@@ -211,15 +214,16 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="current current-course-image">
% if context_course.course_image:
<span class="wrapper-course-image">
<img class="course-image" id="course-image" src="${utils.course_image_url(context_course)}" alt="${_('Course Image')}"/>
<img class="course-image" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/>
</span>
<% ctx_loc = context_course.location %>
<span class="msg msg-help">${_("You can manage this image along with all of your other")} <a href='${upload_asset_url}'>${_("files &amp; uploads")}</a></span>
<span class="msg msg-help">
${_("You can manage this image along with all of your other <a href='{}'>files &amp; uploads</a>").format(upload_asset_url)}
</span>
% else:
<span class="wrapper-course-image">
<img class="course-image placeholder" id="course-image" src="${utils.course_image_url(context_course)}" alt="${_('Course Image')}"/>
<img class="course-image placeholder" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/>
</span>
<span class="msg msg-empty">${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")}</span>
% endif
......@@ -286,16 +290,16 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="bit">
% if context_course:
<%
ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
course_team_url = location.url_reverse('course_team/', '')
course_team_url = course_locator.url_reverse('course_team/', '')
grading_config_url = course_locator.url_reverse('settings/grading/')
advanced_config_url = course_locator.url_reverse('settings/advanced/')
%>
<h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${grading_config_url}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
<li class="nav-item"><a href="${advanced_config_url}">${_("Advanced Settings")}</a></li>
</ul>
</nav>
% endif
......
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from contentstore import utils
from xmodule.modulestore.django import loc_mapper
from django.core.urlresolvers import reverse
%>
<%block name="title">${_("Advanced Settings")}</%block>
<%block name="bodyclass">is-signedin course advanced view-settings</%block>
......@@ -28,7 +26,7 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new AdvancedSettingsModel(${advanced_dict | n}, {parse: true});
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
advancedModel.url = "${advanced_settings_url}";
var editor = new AdvancedSettingsView({
el: $('.settings-advanced'),
......@@ -91,13 +89,15 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<%
ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
details_url = location.url_reverse('settings/details/')
grading_url = location.url_reverse('settings/grading/')
course_team_url = location.url_reverse('course_team/', '')
%>
<h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${details_url}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${grading_url}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
</ul>
</nav>
......
......@@ -7,7 +7,6 @@
from contentstore import utils
from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
from django.core.urlresolvers import reverse
%>
<%block name="header_extras">
......@@ -28,9 +27,11 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
$("label").removeClass("is-focused");
});
var model = new CourseGradingPolicyModel(${course_details|n},{parse:true});
model.urlRoot = '${grading_url}';
var editor = new GradingView({
el: $('.settings-grading'),
model : new CourseGradingPolicyModel(${course_details|n},{parse:true})
model : model
});
editor.render();
......@@ -137,16 +138,16 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
<div class="bit">
% if context_course:
<%
ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
course_team_url = location.url_reverse('course_team/', '')
course_team_url = course_locator.url_reverse('course_team/')
advanced_settings_url = course_locator.url_reverse('settings/advanced/')
detailed_settings_url = course_locator.url_reverse('settings/details/')
%>
<h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${detailed_settings_url}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
<li class="nav-item"><a href="${advanced_settings_url}">${_("Advanced Settings")}</a></li>
</ul>
</nav>
% endif
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment