resolving merge conflicts
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -27,4 +27,7 @@ lms/lib/comment_client/python
|
||||
nosetests.xml
|
||||
cover_html/
|
||||
.idea/
|
||||
chromedriver.log
|
||||
.redcar/
|
||||
chromedriver.log
|
||||
/nbproject
|
||||
ghostdriver.log
|
||||
|
||||
18
.pylintrc
18
.pylintrc
@@ -12,7 +12,7 @@ profile=no
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
ignore=CVS, migrations
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
@@ -33,7 +33,15 @@ load-plugins=
|
||||
# can either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once).
|
||||
disable=E1102,W0142
|
||||
disable=
|
||||
# W0141: Used builtin function 'map'
|
||||
# W0142: Used * or ** magic
|
||||
# R0201: Method could be a function
|
||||
# R0901: Too many ancestors
|
||||
# R0902: Too many instance attributes
|
||||
# R0903: Too few public methods (1/2)
|
||||
# R0904: Too many public methods
|
||||
W0141,W0142,R0201,R0901,R0902,R0903,R0904
|
||||
|
||||
|
||||
[REPORTS]
|
||||
@@ -43,7 +51,7 @@ disable=E1102,W0142
|
||||
output-format=text
|
||||
|
||||
# Include message's id in output
|
||||
include-ids=no
|
||||
include-ids=yes
|
||||
|
||||
# Put messages in a separate file for each module / package specified on the
|
||||
# command line instead of printing them on stdout. Reports (if any) will be
|
||||
@@ -97,7 +105,7 @@ bad-functions=map,filter,apply,input
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Regular expression which should only match correct module level names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
|
||||
|
||||
# Regular expression which should only match correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
@@ -106,7 +114,7 @@ class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Regular expression which should only match correct method names
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*)$
|
||||
|
||||
# Regular expression which should only match correct instance attribute names
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.8.7-p371
|
||||
1.9.3-p374
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -1,4 +1,4 @@
|
||||
source :rubygems
|
||||
source 'https://rubygems.org'
|
||||
gem 'rake', '~> 10.0.3'
|
||||
gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[run]
|
||||
data_file = reports/cms/.coverage
|
||||
source = cms,common/djangoapps
|
||||
omit = cms/envs/*, cms/manage.py, common/djangoapps/*/migrations/*
|
||||
omit = cms/envs/*, cms/manage.py, common/djangoapps/terrain/*, common/djangoapps/*/migrations/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from xmodule.templates import update_templates
|
||||
|
||||
update_templates()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from lxml import html
|
||||
from lxml import html, etree
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest
|
||||
import logging
|
||||
@@ -26,9 +26,9 @@ def get_course_updates(location):
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||
except:
|
||||
course_html_parsed = html.fromstring("<ol></ol>")
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
|
||||
course_upd_collection = []
|
||||
@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None):
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||
except:
|
||||
course_html_parsed = html.fromstring("<ol></ol>")
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
# No try/catch b/c failure generates an error back to client
|
||||
new_html_parsed = html.fromstring('<li><h2>' + update['date'] + '</h2>' + update['content'] + '</li>')
|
||||
@@ -85,13 +85,12 @@ def update_course_updates(location, update, passed_id=None):
|
||||
passed_id = course_updates.location.url() + "/" + str(idx)
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = html.tostring(course_html_parsed)
|
||||
modulestore('direct').update_item(location, course_updates.definition['data'])
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
"content": update['content']}
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
modulestore('direct').update_item(location, course_updates.data)
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
"content" :update['content']}
|
||||
|
||||
def delete_course_update(location, update, passed_id):
|
||||
"""
|
||||
@@ -99,19 +98,19 @@ def delete_course_update(location, update, passed_id):
|
||||
Returns the resulting course_updates b/c their ids change.
|
||||
"""
|
||||
if not passed_id:
|
||||
return HttpResponseBadRequest
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
try:
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# TODO use delete_blank_text parser throughout and cache as a static var in a class
|
||||
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
|
||||
try:
|
||||
course_html_parsed = html.fromstring(course_updates.definition['data'])
|
||||
except:
|
||||
course_html_parsed = html.fromstring("<ol></ol>")
|
||||
course_html_parsed = etree.fromstring(course_updates.data)
|
||||
except etree.XMLSyntaxError:
|
||||
course_html_parsed = etree.fromstring("<ol></ol>")
|
||||
|
||||
if course_html_parsed.tag == 'ol':
|
||||
# ??? Should this use the id in the json or in the url or does it matter?
|
||||
@@ -122,9 +121,9 @@ def delete_course_update(location, update, passed_id):
|
||||
course_html_parsed.remove(element_to_delete)
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = html.tostring(course_html_parsed)
|
||||
course_updates.data = etree.tostring(course_html_parsed)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.definition['data'])
|
||||
store.update_item(location, course_updates.data)
|
||||
|
||||
return get_course_updates(location)
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
Feature: Advanced (manual) course policy
|
||||
In order to specify course policy settings for which no custom user interface exists
|
||||
I want to be able to manually enter JSON key/value pairs
|
||||
|
||||
Scenario: A course author sees default advanced settings
|
||||
Given I have opened a new course in Studio
|
||||
When I select the Advanced Settings
|
||||
Then I see default advanced settings
|
||||
|
||||
Scenario: Add new entries, and they appear alphabetically after save
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
Then the settings are alphabetized
|
||||
|
||||
Scenario: Test cancel editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
And I press the "Cancel" notification button
|
||||
Then the policy key value is unchanged
|
||||
And I reload the page
|
||||
Then the policy key value is unchanged
|
||||
|
||||
Scenario: Test editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
And I press the "Save" notification button
|
||||
Then the policy key value is changed
|
||||
And I reload the page
|
||||
Then the policy key value is changed
|
||||
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value
|
||||
Then it is displayed as formatted
|
||||
And I reload the page
|
||||
Then it is displayed as formatted
|
||||
|
||||
Scenario: Test automatic quoting of non-JSON values
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a non-JSON value not in quotes
|
||||
Then it is displayed as a string
|
||||
And I reload the page
|
||||
Then it is displayed as a string
|
||||
146
cms/djangoapps/contentstore/features/advanced-settings.py
Normal file
146
cms/djangoapps/contentstore/features/advanced-settings.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
import time
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
|
||||
"""
|
||||
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
|
||||
"""
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
KEY_CSS = '.key input.policy-key'
|
||||
VALUE_CSS = 'textarea.json'
|
||||
DISPLAY_NAME_KEY = "display_name"
|
||||
DISPLAY_NAME_VALUE = '"Robot Super Course"'
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select the Advanced Settings$')
|
||||
def i_select_advanced_settings(step):
|
||||
expand_icon_css = 'li.nav-course-settings i.icon-expand'
|
||||
if world.browser.is_element_present_by_css(expand_icon_css):
|
||||
css_click(expand_icon_css)
|
||||
link_css = 'li.nav-course-settings-advanced a'
|
||||
css_click(link_css)
|
||||
|
||||
|
||||
@step('I am on the Advanced Course Settings page in Studio$')
|
||||
def i_am_on_advanced_course_settings(step):
|
||||
step.given('I have opened a new course in Studio')
|
||||
step.given('I select the Advanced Settings')
|
||||
|
||||
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
def is_visible(driver):
|
||||
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
|
||||
|
||||
# def is_invisible(driver):
|
||||
# return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
|
||||
|
||||
css = 'a.%s-button' % name.lower()
|
||||
wait_for(is_visible)
|
||||
time.sleep(float(1))
|
||||
css_click_at(css)
|
||||
|
||||
# is_invisible is not returning a boolean, not working
|
||||
# try:
|
||||
# css_click_at(css)
|
||||
# wait_for(is_invisible)
|
||||
# except WebDriverException, e:
|
||||
# css_click_at(css)
|
||||
# wait_for(is_invisible)
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
def edit_the_value_of_a_policy_key(step):
|
||||
"""
|
||||
It is hard to figure out how to get into the CodeMirror
|
||||
area, so cheat and do it from the policy key field :)
|
||||
"""
|
||||
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
|
||||
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
|
||||
|
||||
|
||||
@step('I create a JSON object as a value$')
|
||||
def create_JSON_object(step):
|
||||
change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
|
||||
|
||||
|
||||
@step('I create a non-JSON value not in quotes$')
|
||||
def create_value_not_in_quotes(step):
|
||||
change_display_name_value(step, 'quote me')
|
||||
|
||||
|
||||
############### RESULTS ####################
|
||||
@step('I see default advanced settings$')
|
||||
def i_see_default_advanced_settings(step):
|
||||
# Test only a few of the existing properties (there are around 34 of them)
|
||||
assert_policy_entries(
|
||||
["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
|
||||
|
||||
|
||||
@step('the settings are alphabetized$')
|
||||
def they_are_alphabetized(step):
|
||||
key_elements = css_find(KEY_CSS)
|
||||
all_keys = []
|
||||
for key in key_elements:
|
||||
all_keys.append(key.value)
|
||||
|
||||
assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
|
||||
|
||||
|
||||
@step('it is displayed as formatted$')
|
||||
def it_is_formatted(step):
|
||||
assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
|
||||
|
||||
|
||||
@step('it is displayed as a string')
|
||||
def it_is_formatted(step):
|
||||
assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
|
||||
|
||||
|
||||
@step(u'the policy key value is unchanged$')
|
||||
def the_policy_key_value_is_unchanged(step):
|
||||
assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
|
||||
|
||||
|
||||
@step(u'the policy key value is changed$')
|
||||
def the_policy_key_value_is_changed(step):
|
||||
assert_equal(get_display_name_value(), '"Robot Super Course X"')
|
||||
|
||||
|
||||
############# HELPERS ###############
|
||||
def assert_policy_entries(expected_keys, expected_values):
|
||||
for counter in range(len(expected_keys)):
|
||||
index = get_index_of(expected_keys[counter])
|
||||
assert_false(index == -1, "Could not find key: " + expected_keys[counter])
|
||||
assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
|
||||
|
||||
|
||||
def get_index_of(expected_key):
|
||||
for counter in range(len(css_find(KEY_CSS))):
|
||||
# Sometimes get stale reference if I hold on to the array of elements
|
||||
key = css_find(KEY_CSS)[counter].value
|
||||
if key == expected_key:
|
||||
return counter
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def get_display_name_value():
|
||||
index = get_index_of(DISPLAY_NAME_KEY)
|
||||
return css_find(VALUE_CSS)[index].value
|
||||
|
||||
|
||||
def change_display_name_value(step, new_value):
|
||||
e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
|
||||
display_name = get_display_name_value()
|
||||
for count in range(len(display_name)):
|
||||
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
|
||||
# Must delete "" before typing the JSON value
|
||||
e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
|
||||
press_the_notification_button(step, "Save")
|
||||
@@ -1,19 +1,22 @@
|
||||
from lettuce import world, step
|
||||
from factories import *
|
||||
from django.core.management import call_command
|
||||
from lettuce.django import django_url
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_equal
|
||||
import xmodule.modulestore.django
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
from terrain.factories import CourseFactory, GroupFactory
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
########### STEP HELPERS ##############
|
||||
|
||||
|
||||
@step('I (?:visit|access|open) the Studio homepage$')
|
||||
def i_visit_the_studio_homepage(step):
|
||||
# To make this go to port 8001, put
|
||||
@@ -44,9 +47,15 @@ def i_press_the_category_delete_icon(step, category):
|
||||
assert False, 'Invalid category: %s' % category
|
||||
css_click(css)
|
||||
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
|
||||
|
||||
def create_studio_user(
|
||||
uname='robot',
|
||||
email='robot+studio@edx.org',
|
||||
@@ -75,9 +84,9 @@ def flush_xmodule_store():
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
xmodule.templates.update_templates()
|
||||
_MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
update_templates()
|
||||
|
||||
|
||||
def assert_css_with_text(css, text):
|
||||
@@ -86,13 +95,50 @@ def assert_css_with_text(css, text):
|
||||
|
||||
|
||||
def css_click(css):
|
||||
world.browser.find_by_css(css).first.click()
|
||||
'''
|
||||
First try to use the regular click method,
|
||||
but if clicking in the middle of an element
|
||||
doesn't work it might be that it thinks some other
|
||||
element is on top of it there so click in the upper left
|
||||
'''
|
||||
try:
|
||||
css_find(css).first.click()
|
||||
except WebDriverException, e:
|
||||
css_click_at(css)
|
||||
|
||||
|
||||
def css_click_at(css, x=10, y=10):
|
||||
'''
|
||||
A method to click at x,y coordinates of the element
|
||||
rather than in the center of the element
|
||||
'''
|
||||
e = css_find(css).first
|
||||
e.action_chains.move_to_element_with_offset(e._element, x, y)
|
||||
e.action_chains.click()
|
||||
e.action_chains.perform()
|
||||
|
||||
|
||||
def css_fill(css, value):
|
||||
world.browser.find_by_css(css).first.fill(value)
|
||||
|
||||
|
||||
def css_find(css):
|
||||
def is_visible(driver):
|
||||
return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
|
||||
|
||||
world.browser.is_element_present_by_css(css, 5)
|
||||
wait_for(is_visible)
|
||||
return world.browser.find_by_css(css)
|
||||
|
||||
|
||||
def wait_for(func):
|
||||
WebDriverWait(world.browser.driver, 5).until(func)
|
||||
|
||||
|
||||
def id_find(id):
|
||||
return world.browser.find_by_id(id)
|
||||
|
||||
|
||||
def clear_courses():
|
||||
flush_xmodule_store()
|
||||
|
||||
@@ -129,9 +175,18 @@ def log_into_studio(
|
||||
|
||||
|
||||
def create_a_course():
|
||||
css_click('a.new-course-button')
|
||||
fill_in_course_info()
|
||||
css_click('input.new-course-save')
|
||||
c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
# Add the user to the instructor group of the course
|
||||
# so they will have the permissions to see it in studio
|
||||
g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
|
||||
u = get_user_by_email('robot+studio@edx.org')
|
||||
u.groups.add(g)
|
||||
u.save()
|
||||
world.browser.reload()
|
||||
|
||||
course_link_css = 'span.class-name'
|
||||
css_click(course_link_css)
|
||||
course_title_css = 'span.course-title'
|
||||
assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
|
||||
|
||||
@@ -146,6 +201,7 @@ def add_section(name='My Section'):
|
||||
span_css = 'span.section-name-span'
|
||||
assert_true(world.browser.is_element_present_by_css(span_css, 5))
|
||||
|
||||
|
||||
def add_subsection(name='Subsection One'):
|
||||
css = 'a.new-subsection-item'
|
||||
css_click(css)
|
||||
|
||||
@@ -59,4 +59,4 @@ def i_am_on_tab(step, tab_name):
|
||||
@step('I see a link for adding a new section$')
|
||||
def i_see_new_section_link(step):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
assert_css_with_text(link_css, 'New Section')
|
||||
assert_css_with_text(link_css, '+ New Section')
|
||||
|
||||
@@ -11,6 +11,14 @@ Feature: Create Section
|
||||
And I see a release date for my section
|
||||
And I see a link to create a new subsection
|
||||
|
||||
Scenario: Add a new section (with a quote in the name) to a course (bug #216)
|
||||
Given I have opened a new course in Studio
|
||||
When I click the New Section link
|
||||
And I enter a section name with a quote and click save
|
||||
Then I see my section name with a quote on the Courseware page
|
||||
And I click to edit the section name
|
||||
Then I see the complete section name with a quote in the editor
|
||||
|
||||
Scenario: Edit section release date
|
||||
Given I have opened a new course in Studio
|
||||
And I have added a new section
|
||||
@@ -18,9 +26,10 @@ Feature: Create Section
|
||||
And I save a new section release date
|
||||
Then the section release date is updated
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete section
|
||||
Given I have opened a new course in Studio
|
||||
And I have added a new section
|
||||
When I press the "section" delete icon
|
||||
And I confirm the alert
|
||||
Then the section does not exist
|
||||
Then the section does not exist
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
import time
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
|
||||
@step('I click the new section link$')
|
||||
def i_click_new_section_link(step):
|
||||
link_css = 'a.new-courseware-section-button'
|
||||
@@ -19,10 +15,12 @@ def i_click_new_section_link(step):
|
||||
|
||||
@step('I enter the section name and click save$')
|
||||
def i_save_section_name(step):
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
css_fill(name_css, 'My Section')
|
||||
css_click(save_css)
|
||||
save_section_name('My Section')
|
||||
|
||||
|
||||
@step('I enter a section name with a quote and click save$')
|
||||
def i_save_section_name_with_quote(step):
|
||||
save_section_name('Section with "Quote"')
|
||||
|
||||
|
||||
@step('I have added a new section$')
|
||||
@@ -41,18 +39,39 @@ def i_save_a_new_section_release_date(step):
|
||||
date_css = 'input.start-date.date.hasDatepicker'
|
||||
time_css = 'input.start-time.time.ui-timepicker-input'
|
||||
css_fill(date_css, '12/25/2013')
|
||||
# click here to make the calendar go away
|
||||
css_click(time_css)
|
||||
# hit TAB to get to the time field
|
||||
e = css_find(date_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
css_fill(time_css, '12:00am')
|
||||
css_click('a.save-button')
|
||||
e = css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
world.browser.click_link_by_text('Save')
|
||||
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('I see my section on the Courseware page$')
|
||||
def i_see_my_section_on_the_courseware_page(step):
|
||||
section_css = 'span.section-name-span'
|
||||
assert_css_with_text(section_css, 'My Section')
|
||||
see_my_section_on_the_courseware_page('My Section')
|
||||
|
||||
|
||||
@step('I see my section name with a quote on the Courseware page$')
|
||||
def i_see_my_section_name_with_quote_on_the_courseware_page(step):
|
||||
see_my_section_on_the_courseware_page('Section with "Quote"')
|
||||
|
||||
|
||||
@step('I click to edit the section name$')
|
||||
def i_click_to_edit_section_name(step):
|
||||
css_click('span.section-name-span')
|
||||
|
||||
|
||||
@step('I see the complete section name with a quote in the editor$')
|
||||
def i_see_complete_section_name_with_quote_in_editor(step):
|
||||
css = '.edit-section-name'
|
||||
assert world.browser.is_element_present_by_css(css, 5)
|
||||
assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"')
|
||||
|
||||
|
||||
@step('the section does not exist$')
|
||||
@@ -93,4 +112,18 @@ def the_section_release_date_picker_not_visible(step):
|
||||
def the_section_release_date_is_updated(step):
|
||||
css = 'span.published-status'
|
||||
status_text = world.browser.find_by_css(css).text
|
||||
assert status_text == 'Will Release: 12/25/2013 at 12:00am'
|
||||
assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am')
|
||||
|
||||
|
||||
############ HELPER METHODS ###################
|
||||
|
||||
def save_section_name(name):
|
||||
name_css = '.new-section-name'
|
||||
save_css = '.new-section-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
|
||||
|
||||
def see_my_section_on_the_courseware_page(name):
|
||||
section_css = 'span.section-name-span'
|
||||
assert_css_with_text(section_css, name)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
|
||||
@step('I fill in the registration form$')
|
||||
@@ -13,10 +14,11 @@ def i_fill_in_the_registration_form(step):
|
||||
|
||||
@step('I press the Create My Account button on the registration form$')
|
||||
def i_press_the_button_on_the_registration_form(step):
|
||||
register_form = world.browser.find_by_css('form#register_form')
|
||||
submit_css = 'button#submit'
|
||||
register_form.find_by_css(submit_css).click()
|
||||
|
||||
submit_css = 'form#register_form button#submit'
|
||||
# Workaround for click not working on ubuntu
|
||||
# for some unknown reason.
|
||||
e = css_find(submit_css)
|
||||
e.type(' ')
|
||||
|
||||
@step('I should see be on the studio home page$')
|
||||
def i_should_see_be_on_the_studio_home_page(step):
|
||||
|
||||
@@ -21,6 +21,7 @@ Feature: Overview Toggle Section
|
||||
Then I see the "Collapse All Sections" link
|
||||
And all sections are expanded
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Collapse link is not removed after last section of a course is deleted
|
||||
Given I have a course with 1 section
|
||||
And I navigate to the course overview page
|
||||
|
||||
@@ -9,6 +9,15 @@ Feature: Create Subsection
|
||||
And I enter the subsection name and click save
|
||||
Then I see my subsection on the Courseware page
|
||||
|
||||
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
|
||||
Given I have opened a new course section in Studio
|
||||
When I click the New Subsection link
|
||||
And I enter a subsection name with a quote and click save
|
||||
Then I see my subsection name with a quote on the Courseware page
|
||||
And I click to edit the subsection name
|
||||
Then I see the complete subsection name with a quote in the editor
|
||||
|
||||
@skip-phantom
|
||||
Scenario: Delete a subsection
|
||||
Given I have opened a new course section in Studio
|
||||
And I have added a new subsection
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
@@ -20,28 +21,60 @@ def i_click_the_new_subsection_link(step):
|
||||
|
||||
@step('I enter the subsection name and click save$')
|
||||
def i_save_subsection_name(step):
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css, 'Subsection One')
|
||||
css_click(save_css)
|
||||
save_subsection_name('Subsection One')
|
||||
|
||||
|
||||
@step('I enter a subsection name with a quote and click save$')
|
||||
def i_save_subsection_name_with_quote(step):
|
||||
save_subsection_name('Subsection With "Quote"')
|
||||
|
||||
|
||||
@step('I click to edit the subsection name$')
|
||||
def i_click_to_edit_subsection_name(step):
|
||||
css_click('span.subsection-name-value')
|
||||
|
||||
|
||||
@step('I see the complete subsection name with a quote in the editor$')
|
||||
def i_see_complete_subsection_name_with_quote_in_editor(step):
|
||||
css = '.subsection-display-name-input'
|
||||
assert world.browser.is_element_present_by_css(css, 5)
|
||||
assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"')
|
||||
|
||||
|
||||
@step('I have added a new subsection$')
|
||||
def i_have_added_a_new_subsection(step):
|
||||
add_subsection()
|
||||
|
||||
|
||||
############ ASSERTIONS ###################
|
||||
|
||||
|
||||
@step('I see my subsection on the Courseware page$')
|
||||
def i_see_my_subsection_on_the_courseware_page(step):
|
||||
css = 'span.subsection-name'
|
||||
assert world.browser.is_element_present_by_css(css)
|
||||
css = 'span.subsection-name-value'
|
||||
assert_css_with_text(css, 'Subsection One')
|
||||
see_subsection_name('Subsection One')
|
||||
|
||||
|
||||
@step('I see my subsection name with a quote on the Courseware page$')
|
||||
def i_see_my_subsection_name_with_quote_on_the_courseware_page(step):
|
||||
see_subsection_name('Subsection With "Quote"')
|
||||
|
||||
|
||||
@step('the subsection does not exist$')
|
||||
def the_subsection_does_not_exist(step):
|
||||
css = 'span.subsection-name'
|
||||
assert world.browser.is_element_not_present_by_css(css)
|
||||
|
||||
|
||||
############ HELPER METHODS ###################
|
||||
|
||||
def save_subsection_name(name):
|
||||
name_css = 'input.new-subsection-name-input'
|
||||
save_css = 'input.new-subsection-name-save'
|
||||
css_fill(name_css, name)
|
||||
css_click(save_css)
|
||||
|
||||
def see_subsection_name(name):
|
||||
css = 'span.subsection-name'
|
||||
assert world.browser.is_element_present_by_css(css)
|
||||
css = 'span.subsection-name-value'
|
||||
assert_css_with_text(css, name)
|
||||
|
||||
@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from prompt import query_yes_no
|
||||
from .prompt import query_yes_no
|
||||
|
||||
from auth.authz import _delete_course_group
|
||||
|
||||
@@ -17,22 +17,29 @@ from auth.authz import _delete_course_group
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Delete a MongoDB backed course'''
|
||||
help = '''Delete a MongoDB backed course'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 1:
|
||||
raise CommandError("delete_course requires one argument: <location>")
|
||||
if len(args) != 1 and len(args) != 2:
|
||||
raise CommandError("delete_course requires one or more arguments: <location> |commit|")
|
||||
|
||||
loc_str = args[0]
|
||||
|
||||
commit = False
|
||||
if len(args) == 2:
|
||||
commit = args[1] == 'commit'
|
||||
|
||||
if commit:
|
||||
print 'Actually going to delete the course from DB....'
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
if delete_course(ms, cs, loc) == True:
|
||||
print 'removing User permissions from course....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
_delete_course_group(loc)
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
if delete_course(ms, cs, loc, commit) == True:
|
||||
print 'removing User permissions from course....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
if commit:
|
||||
_delete_course_group(loc)
|
||||
|
||||
@@ -13,7 +13,7 @@ def query_yes_no(question, default="yes"):
|
||||
"""
|
||||
valid = {"yes":True, "y":True, "ye":True,
|
||||
"no":False, "n":False}
|
||||
if default == None:
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from xmodule.templates import update_templates
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Imports and updates the Studio component templates from the code pack and put in the DB'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
update_templates()
|
||||
@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
|
||||
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
|
||||
module = store.clone_item(template_location, location)
|
||||
|
||||
data = module.definition['data']
|
||||
data = module.data
|
||||
if rewrite_static_links:
|
||||
data = replace_static_urls(
|
||||
module.definition['data'],
|
||||
module.data,
|
||||
None,
|
||||
course_namespace=Location([
|
||||
module.location.tag,
|
||||
@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
|
||||
return {
|
||||
'id': module.location.url(),
|
||||
'data': data,
|
||||
'metadata': module.metadata
|
||||
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
|
||||
'metadata': module._model_data._kvs._metadata
|
||||
}
|
||||
|
||||
|
||||
@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
|
||||
# 'apply' the submitted metadata, so we don't end up deleting system metadata
|
||||
if post_data.get('metadata') is not None:
|
||||
posted_metadata = post_data['metadata']
|
||||
|
||||
|
||||
# update existing metadata with submitted metadata (which can be partial)
|
||||
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
|
||||
for metadata_key in posted_metadata.keys():
|
||||
|
||||
for metadata_key, value in posted_metadata.items():
|
||||
|
||||
# let's strip out any metadata fields from the postback which have been identified as system metadata
|
||||
# and therefore should not be user-editable, so we should accept them back from the client
|
||||
if metadata_key in module.system_metadata_fields:
|
||||
del posted_metadata[metadata_key]
|
||||
elif posted_metadata[metadata_key] is None:
|
||||
# remove both from passed in collection as well as the collection read in from the modulestore
|
||||
if metadata_key in module.metadata:
|
||||
del module.metadata[metadata_key]
|
||||
if metadata_key in module._model_data:
|
||||
del module._model_data[metadata_key]
|
||||
del posted_metadata[metadata_key]
|
||||
|
||||
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
||||
module.metadata.update(posted_metadata)
|
||||
|
||||
else:
|
||||
module._model_data[metadata_key] = value
|
||||
|
||||
# commit to datastore
|
||||
store.update_metadata(location, module.metadata)
|
||||
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
|
||||
store.update_metadata(location, module._model_data._kvs._metadata)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
from factory import Factory
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed)
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
name = 'Robot Studio'
|
||||
courseware = 'course.xml'
|
||||
|
||||
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid4().hex
|
||||
|
||||
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
email = 'robot@edx.org'
|
||||
password = 'test'
|
||||
first_name = 'Robot'
|
||||
last_name = 'Tester'
|
||||
is_staff = False
|
||||
is_active = True
|
||||
is_superuser = False
|
||||
last_login = datetime.now()
|
||||
date_joined = datetime.now()
|
||||
|
||||
|
||||
class GroupFactory(Factory):
|
||||
FACTORY_FOR = Group
|
||||
|
||||
name = 'test_group'
|
||||
|
||||
|
||||
class CourseEnrollmentAllowedFactory(Factory):
|
||||
FACTORY_FOR = CourseEnrollmentAllowed
|
||||
|
||||
email = 'test@edx.org'
|
||||
course_id = 'edX/test/2012_Fall'
|
||||
@@ -5,29 +5,28 @@ from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempfile import mkdtemp
|
||||
from tempdir import mkdtemp_clean
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from mock import Mock
|
||||
from json import dumps, loads
|
||||
from json import loads
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from utils import ModuleStoreTestCase, parse_json
|
||||
from .utils import ModuleStoreTestCase, parse_json
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
@@ -63,7 +62,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
@@ -82,8 +80,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
module_store = modulestore('direct')
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
# reverse the ordering
|
||||
reverse_tabs = []
|
||||
@@ -91,9 +89,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
if tab['type'] == 'static_tab':
|
||||
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
|
||||
|
||||
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
|
||||
self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
|
||||
|
||||
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
# compare to make sure that the tabs information is in the expected order after the server call
|
||||
course_tabs = []
|
||||
@@ -103,29 +101,61 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(reverse_tabs, course_tabs)
|
||||
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertTrue(sequential.location.url() in chapter.children)
|
||||
|
||||
self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
|
||||
"application/json")
|
||||
|
||||
found = False
|
||||
try:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
found = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
|
||||
|
||||
# make sure the parent no longer points to the child object which was deleted
|
||||
self.assertFalse(sequential.location.url() in chapter.children)
|
||||
|
||||
|
||||
|
||||
def test_about_overrides(self):
|
||||
'''
|
||||
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
|
||||
while there is a base definition in /about/effort.html
|
||||
'''
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
ms = modulestore('direct')
|
||||
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.definition['data'], '6 hours')
|
||||
module_store = modulestore('direct')
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
|
||||
self.assertEqual(effort.data, '6 hours')
|
||||
|
||||
# this one should be in a non-override folder
|
||||
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||
self.assertEqual(effort.definition['data'], 'TBD')
|
||||
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
|
||||
self.assertEqual(effort.data, 'TBD')
|
||||
|
||||
def test_remove_hide_progress_tab(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = ms.get_item(source_location)
|
||||
self.assertNotIn('hide_progress_tab', course.metadata)
|
||||
course = module_store.get_item(source_location)
|
||||
self.assertFalse(course.hide_progress_tab)
|
||||
|
||||
def test_clone_course(self):
|
||||
|
||||
@@ -143,19 +173,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
|
||||
|
||||
clone_course(ms, cs, source_location, dest_location)
|
||||
clone_course(module_store, content_store, source_location, dest_location)
|
||||
|
||||
# now loop through all the units in the course and verify that the clone can render them, which
|
||||
# means the objects are at least present
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||
clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
|
||||
self.assertGreater(len(clone_items), 0)
|
||||
for descriptor in items:
|
||||
new_loc = descriptor.location._replace(org='MITx', course='999')
|
||||
@@ -166,14 +196,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_delete_course(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
delete_course(ms, cs, location)
|
||||
delete_course(module_store, content_store, location, commit=True)
|
||||
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertEqual(len(items), 0)
|
||||
|
||||
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
|
||||
@@ -188,54 +218,54 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.assertTrue(fs.exists(item.location.name + filename_suffix))
|
||||
|
||||
def test_export_course(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(ms, cs, location, root_dir, 'test_export')
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
|
||||
self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
|
||||
|
||||
# check for custom_tags
|
||||
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
|
||||
|
||||
# check for graiding_policy.json
|
||||
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
|
||||
self.assertTrue(fs.exists('grading_policy.json'))
|
||||
|
||||
course = ms.get_item(location)
|
||||
course = module_store.get_item(location)
|
||||
# compare what's on disk compared to what we have in our course
|
||||
with fs.open('grading_policy.json','r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
|
||||
with fs.open('grading_policy.json', 'r') as grading_policy:
|
||||
on_disk = loads(grading_policy.read())
|
||||
self.assertEqual(on_disk, course.grading_policy)
|
||||
|
||||
#check for policy.json
|
||||
self.assertTrue(fs.exists('policy.json'))
|
||||
|
||||
# compare what's on disk to what we have in the course module
|
||||
with fs.open('policy.json','r') as course_policy:
|
||||
with fs.open('policy.json', 'r') as course_policy:
|
||||
on_disk = loads(course_policy.read())
|
||||
self.assertIn('course/6.002_Spring_2012', on_disk)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
|
||||
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
|
||||
|
||||
# remove old course
|
||||
delete_course(ms, cs, location)
|
||||
delete_course(module_store, content_store, location)
|
||||
|
||||
# reimport
|
||||
import_from_xml(ms, root_dir, ['test_export'])
|
||||
import_from_xml(module_store, root_dir, ['test_export'])
|
||||
|
||||
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
@@ -245,11 +275,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
# import a test course
|
||||
import_from_xml(ms, 'common/test/data/', ['full'])
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
|
||||
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
|
||||
|
||||
@@ -263,6 +293,34 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# note, we know the link it should be because that's what in the 'full' course in the test data
|
||||
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
|
||||
|
||||
def test_export_course_with_unknown_metadata(self):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
course = module_store.get_item(location)
|
||||
|
||||
metadata = own_metadata(course)
|
||||
# add a bool piece of unknown metadata so we can verify we don't throw an exception
|
||||
metadata['new_metadata'] = True
|
||||
|
||||
module_store.update_metadata(location, metadata)
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
exported = False
|
||||
try:
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
exported = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.assertTrue(exported)
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
@@ -342,7 +400,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<h1>My Courses</h1>',
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
@@ -401,7 +459,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
def test_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
problem_data = {
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
@@ -418,22 +476,77 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
|
||||
context = problem.get_context()
|
||||
self.assertIn('markdown', context, "markdown is missing from context")
|
||||
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
|
||||
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
|
||||
|
||||
def test_import_metadata_with_attempts_empty_string(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
module_store = modulestore('direct')
|
||||
did_load_item = False
|
||||
try:
|
||||
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
|
||||
did_load_item = True
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
# make sure we found the item (e.g. it didn't error while loading)
|
||||
self.assertTrue(did_load_item)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
|
||||
|
||||
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
|
||||
|
||||
# let's assert on the metadata_inheritance on an existing vertical
|
||||
for vertical in verticals:
|
||||
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
|
||||
|
||||
self.assertGreater(len(verticals), 0)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
parent = verticals[0]
|
||||
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
|
||||
|
||||
# flush the cache
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
# check for grace period definition which should be defined at the course level
|
||||
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
|
||||
|
||||
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
|
||||
|
||||
#
|
||||
# now let's define an override at the leaf node level
|
||||
#
|
||||
new_module.lms.graceperiod = timedelta(1)
|
||||
module_store.update_metadata(new_module.location, own_metadata(new_module))
|
||||
|
||||
# flush the cache and refetch
|
||||
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
|
||||
new_module = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
|
||||
|
||||
|
||||
class TemplateTestCase(ModuleStoreTestCase):
|
||||
|
||||
def test_template_cleanup(self):
|
||||
ms = modulestore('direct')
|
||||
def test_template_cleanup(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
# insert a bogus template in the store
|
||||
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
ms.clone_item(source_template_location, bogus_template_location)
|
||||
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
module_store.clone_item(source_template_location, bogus_template_location)
|
||||
|
||||
verify_create = module_store.get_item(bogus_template_location)
|
||||
self.assertIsNotNone(verify_create)
|
||||
|
||||
# now run cleanup
|
||||
@@ -442,10 +555,8 @@ class TemplateTestCase(ModuleStoreTestCase):
|
||||
# now try to find dangling template, it should not be in DB any longer
|
||||
asserted = False
|
||||
try:
|
||||
verify_create = ms.get_item(bogus_template_location)
|
||||
verify_create = module_store.get_item(bogus_template_location)
|
||||
except ItemNotFoundError:
|
||||
asserted = True
|
||||
|
||||
self.assertTrue(asserted)
|
||||
|
||||
|
||||
self.assertTrue(asserted)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import datetime
|
||||
import time
|
||||
import json
|
||||
import calendar
|
||||
import copy
|
||||
from util import converters
|
||||
from util.converters import jsdate_to_time
|
||||
@@ -11,17 +9,20 @@ from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
import xmodule
|
||||
from xmodule.modulestore import Location
|
||||
from cms.djangoapps.models.settings.course_details import (CourseDetails,
|
||||
from models.settings.course_details import (CourseDetails,
|
||||
CourseSettingsEncoder)
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from django.test import TestCase
|
||||
from utils import ModuleStoreTestCase
|
||||
from .utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
|
||||
class ConvertersTestCase(TestCase):
|
||||
@@ -245,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
|
||||
|
||||
test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
|
||||
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
|
||||
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
|
||||
print test_grader.grace_period, altered_grader.grace_period
|
||||
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
|
||||
|
||||
def test_update_grader_from_json(self):
|
||||
@@ -261,3 +263,64 @@ class CourseGradingTest(CourseTestCase):
|
||||
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
# add in the full class too
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None])
|
||||
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
|
||||
|
||||
test_model = CourseMetadata.fetch(self.fullcourse_location)
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
|
||||
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
|
||||
self.assertIn('showanswer', test_model, 'showanswer field ')
|
||||
self.assertIn('xqa_key', test_model, 'xqa_key field ')
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start A",
|
||||
"testcenter_info" : { "c" : "test" },
|
||||
"days_early_for_beta" : 2})
|
||||
self.update_check(test_model)
|
||||
# try fresh fetch to ensure persistence
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.update_check(test_model)
|
||||
# now change some of the existing metadata
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start B",
|
||||
"display_name" : "jolly roger"})
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
|
||||
|
||||
def update_check(self, test_model):
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
|
||||
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
|
||||
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
|
||||
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
|
||||
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
|
||||
|
||||
|
||||
def test_delete_key(self):
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
# ensure no harm
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
|
||||
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
|
||||
# check for deletion effectiveness
|
||||
self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
|
||||
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from cms.djangoapps.contentstore import utils
|
||||
from contentstore import utils
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@ from django.test.client import Client
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempfile import mkdtemp
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
@@ -25,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
from .utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
|
||||
|
||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import copy
|
||||
from time import time
|
||||
from uuid import uuid4
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
|
||||
@@ -20,13 +20,12 @@ class ModuleStoreTestCase(TestCase):
|
||||
def _pre_setup(self):
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
|
||||
# Use the current seconds since epoch to differentiate
|
||||
# Use a uuid to differentiate
|
||||
# the mongo collections on jenkins.
|
||||
sec_since_epoch = '%s' % int(time() * 100)
|
||||
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
self.test_MODULESTORE = self.orig_MODULESTORE
|
||||
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
|
||||
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
|
||||
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
settings.MODULESTORE = self.test_MODULESTORE
|
||||
|
||||
# Flush and initialize the module store
|
||||
|
||||
@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
|
||||
# make sure we found exactly one match on this above course search
|
||||
found_cnt = len(courses)
|
||||
if found_cnt == 0:
|
||||
raise BaseException('Could not find course at {0}'.format(course_search_location))
|
||||
raise Exception('Could not find course at {0}'.format(course_search_location))
|
||||
|
||||
if found_cnt > 1:
|
||||
raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
|
||||
|
||||
location = courses[0].location
|
||||
|
||||
@@ -75,12 +75,20 @@ def get_course_for_item(location):
|
||||
return courses[0]
|
||||
|
||||
|
||||
def get_lms_link_for_item(location, preview=False):
|
||||
def get_lms_link_for_item(location, preview=False, course_id=None):
|
||||
if course_id is None:
|
||||
course_id = get_course_id(location)
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
preview='preview.' if preview else '',
|
||||
lms_base=settings.LMS_BASE,
|
||||
course_id=get_course_id(location),
|
||||
if preview:
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
else:
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=course_id,
|
||||
location=Location(location)
|
||||
)
|
||||
else:
|
||||
@@ -128,7 +136,7 @@ def compute_unit_state(unit):
|
||||
'private' content is editabled and not visible in the LMS
|
||||
"""
|
||||
|
||||
if unit.metadata.get('is_draft', False):
|
||||
if unit.cms.is_draft:
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
|
||||
@@ -18,7 +18,8 @@ from django.core.files.temp import NamedTemporaryFile
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.context_processors import csrf
|
||||
@@ -28,11 +29,15 @@ from django.conf import settings
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xblock.core import Scope
|
||||
from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
import static_replace
|
||||
from external_auth.views import ssl_login_shortcut
|
||||
from xmodule.modulestore.mongo import MongoUsage
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -54,12 +59,12 @@ from contentstore.course_info_model import get_course_updates,\
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.timeparse import stringify_time
|
||||
from contentstore.module_info_model import get_module_info, set_module_info
|
||||
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
||||
from models.settings.course_details import CourseDetails,\
|
||||
CourseSettingsEncoder
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
from lxml import etree
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
from django.shortcuts import redirect
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
|
||||
@@ -68,6 +73,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading']
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
@@ -82,12 +91,14 @@ def signup(request):
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
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):
|
||||
@@ -100,21 +111,23 @@ def login_page(request):
|
||||
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
|
||||
})
|
||||
|
||||
|
||||
def howitworks(request):
|
||||
if request.user.is_authenticated():
|
||||
return index(request)
|
||||
else:
|
||||
else:
|
||||
return render_to_response('howitworks.html', {})
|
||||
|
||||
# ==== Views for any logged-in user ==================================
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def index(request):
|
||||
"""
|
||||
List all courses available to the logged in user
|
||||
"""
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
|
||||
|
||||
# filter out courses that we don't have access too
|
||||
def course_filter(course):
|
||||
@@ -127,11 +140,12 @@ def index(request):
|
||||
|
||||
return render_to_response('index.html', {
|
||||
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
||||
'courses': [(course.metadata.get('display_name'),
|
||||
'courses': [(course.display_name,
|
||||
reverse('course_index', args=[
|
||||
course.location.org,
|
||||
course.location.course,
|
||||
course.location.name]))
|
||||
course.location.name]),
|
||||
get_lms_link_for_item(course.location, course_id=course.location.course_id))
|
||||
for course in courses],
|
||||
'user': request.user,
|
||||
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
|
||||
@@ -140,6 +154,7 @@ def index(request):
|
||||
|
||||
# ==== Views with per-item permissions================================
|
||||
|
||||
|
||||
def has_access(user, location, role=STAFF_ROLE_NAME):
|
||||
'''
|
||||
Return True if user allowed to access this piece of data
|
||||
@@ -172,6 +187,8 @@ def course_index(request, org, course, name):
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
lms_link = get_lms_link_for_item(location)
|
||||
|
||||
upload_asset_callback_url = reverse('upload_asset', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
@@ -184,6 +201,7 @@ def course_index(request, org, course, name):
|
||||
return render_to_response('overview.html', {
|
||||
'active_tab': 'courseware',
|
||||
'context_course': course,
|
||||
'lms_link': lms_link,
|
||||
'sections': sections,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
@@ -226,8 +244,13 @@ def edit_subsection(request, location):
|
||||
|
||||
# remove all metadata from the generic dictionary that is presented in a more normalized UI
|
||||
|
||||
policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
|
||||
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
|
||||
policy_metadata = dict(
|
||||
(field.name, field.read_from(item))
|
||||
for field
|
||||
in item.fields
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and
|
||||
field.scope == Scope.settings
|
||||
)
|
||||
|
||||
can_view_live = False
|
||||
subsection_units = item.get_children()
|
||||
@@ -277,14 +300,34 @@ def edit_unit(request, location):
|
||||
|
||||
component_templates = defaultdict(list)
|
||||
|
||||
# Check if there are any advanced modules specified in the course policy. These modules
|
||||
# should be specified as a list of strings, where the strings are the names of the modules
|
||||
# in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
|
||||
course_advanced_keys = course.advanced_modules
|
||||
|
||||
# Set component types according to course policy file
|
||||
component_types = list(COMPONENT_TYPES)
|
||||
if isinstance(course_advanced_keys, list):
|
||||
course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
|
||||
if len(course_advanced_keys) > 0:
|
||||
component_types.append(ADVANCED_COMPONENT_CATEGORY)
|
||||
else:
|
||||
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
|
||||
|
||||
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
|
||||
for template in templates:
|
||||
if template.location.category in COMPONENT_TYPES:
|
||||
component_templates[template.location.category].append((
|
||||
template.display_name,
|
||||
category = template.location.category
|
||||
|
||||
if category in course_advanced_keys:
|
||||
category = ADVANCED_COMPONENT_CATEGORY
|
||||
|
||||
if category in component_types:
|
||||
#This is a hack to create categories for different xmodules
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
'markdown' in template.metadata,
|
||||
'empty' in template.metadata
|
||||
hasattr(template, 'markdown') and template.markdown is not None,
|
||||
template.cms.empty,
|
||||
))
|
||||
|
||||
components = [
|
||||
@@ -313,8 +356,11 @@ def edit_unit(request, location):
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview='preview.',
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
|
||||
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
@@ -325,11 +371,6 @@ def edit_unit(request, location):
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
|
||||
try:
|
||||
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
|
||||
except TypeError:
|
||||
published_date = None
|
||||
|
||||
return render_to_response('unit.html', {
|
||||
'context_course': course,
|
||||
'active_tab': 'courseware',
|
||||
@@ -340,11 +381,11 @@ def edit_unit(request, location):
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
|
||||
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
|
||||
'section': containing_section,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'unit_state': unit_state,
|
||||
'published_date': published_date,
|
||||
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
|
||||
})
|
||||
|
||||
|
||||
@@ -409,9 +450,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
dispatch: The action to execute
|
||||
"""
|
||||
|
||||
instance_state, shared_state = load_preview_state(request, preview_id, location)
|
||||
descriptor = modulestore().get_item(location)
|
||||
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
instance = load_preview_module(request, preview_id, descriptor)
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
||||
@@ -422,46 +462,9 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
|
||||
def load_preview_state(request, preview_id, location):
|
||||
"""
|
||||
Load the state of a preview module from the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
instance_state = request.session['preview_states'][preview_id, location].get('instance')
|
||||
shared_state = request.session['preview_states'][preview_id, location].get('shared')
|
||||
|
||||
return instance_state, shared_state
|
||||
|
||||
|
||||
def save_preview_state(request, preview_id, location, instance_state, shared_state):
|
||||
"""
|
||||
Save the state of a preview module to the request
|
||||
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: The Location of the module to dispatch to
|
||||
instance_state: The instance state to save
|
||||
shared_state: The shared state to save
|
||||
"""
|
||||
if 'preview_states' not in request.session:
|
||||
request.session['preview_states'] = defaultdict(dict)
|
||||
|
||||
# request.session doesn't notice indirect changes; so, must set its dict w/ every change to get
|
||||
# it to persist: http://www.djangobook.com/en/2.0/chapter14.html
|
||||
preview_states = request.session['preview_states']
|
||||
preview_states[preview_id, location]['instance'] = instance_state
|
||||
preview_states[preview_id, location]['shared'] = shared_state
|
||||
request.session['preview_states'] = preview_states # make session mgmt notice the update
|
||||
|
||||
|
||||
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
"""
|
||||
Render a template using the LMS MAKO_TEMPLATES
|
||||
@@ -469,6 +472,33 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
||||
|
||||
|
||||
class SessionKeyValueStore(KeyValueStore):
|
||||
def __init__(self, request, model_data):
|
||||
self._model_data = model_data
|
||||
self._session = request.session
|
||||
|
||||
def get(self, key):
|
||||
try:
|
||||
return self._model_data[key.field_name]
|
||||
except (KeyError, InvalidScopeError):
|
||||
return self._session[tuple(key)]
|
||||
|
||||
def set(self, key, value):
|
||||
try:
|
||||
self._model_data[key.field_name] = value
|
||||
except (KeyError, InvalidScopeError):
|
||||
self._session[tuple(key)] = value
|
||||
|
||||
def delete(self, key):
|
||||
try:
|
||||
del self._model_data[key.field_name]
|
||||
except (KeyError, InvalidScopeError):
|
||||
del self._session[tuple(key)]
|
||||
|
||||
def has(self, key):
|
||||
return key in self._model_data or key in self._session
|
||||
|
||||
|
||||
def preview_module_system(request, preview_id, descriptor):
|
||||
"""
|
||||
Returns a ModuleSystem for the specified descriptor that is specialized for
|
||||
@@ -479,6 +509,14 @@ def preview_module_system(request, preview_id, descriptor):
|
||||
descriptor: An XModuleDescriptor
|
||||
"""
|
||||
|
||||
def preview_model_data(descriptor):
|
||||
return DbModel(
|
||||
SessionKeyValueStore(request, descriptor._model_data),
|
||||
descriptor.module_class,
|
||||
preview_id,
|
||||
MongoUsage(preview_id, descriptor.location.url()),
|
||||
)
|
||||
|
||||
return ModuleSystem(
|
||||
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
|
||||
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
|
||||
@@ -489,6 +527,7 @@ def preview_module_system(request, preview_id, descriptor):
|
||||
debug=True,
|
||||
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
|
||||
user=request.user,
|
||||
xblock_model_data=preview_model_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -501,11 +540,11 @@ def get_preview_module(request, preview_id, descriptor):
|
||||
preview_id (str): An identifier specifying which preview this module is used for
|
||||
location: A Location
|
||||
"""
|
||||
instance_state, shared_state = descriptor.get_sample_state()[0]
|
||||
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
|
||||
|
||||
return load_preview_module(request, preview_id, descriptor)
|
||||
|
||||
|
||||
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
|
||||
def load_preview_module(request, preview_id, descriptor):
|
||||
"""
|
||||
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
|
||||
|
||||
@@ -517,12 +556,13 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
"""
|
||||
system = preview_module_system(request, preview_id, descriptor)
|
||||
try:
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
module = descriptor.xmodule(system)
|
||||
except:
|
||||
log.debug("Unable to load preview module", exc_info=True)
|
||||
module = ErrorDescriptor.from_descriptor(
|
||||
descriptor,
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
).xmodule_constructor(system)(None, None)
|
||||
).xmodule(system)
|
||||
|
||||
# cdodge: Special case
|
||||
if module.location.category == 'static_tab':
|
||||
@@ -540,11 +580,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
module.metadata.get('data_dir', module.location.course),
|
||||
getattr(module, 'data_dir', module.location.course),
|
||||
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
|
||||
)
|
||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||
module.get_instance_state(), module.get_shared_state())
|
||||
|
||||
return module
|
||||
|
||||
@@ -558,7 +596,7 @@ def get_module_previews(request, descriptor):
|
||||
"""
|
||||
preview_html = []
|
||||
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
|
||||
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
|
||||
module = load_preview_module(request, str(idx), descriptor)
|
||||
preview_html.append(module.get_html())
|
||||
return preview_html
|
||||
|
||||
@@ -605,6 +643,19 @@ def delete_item(request):
|
||||
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
|
||||
modulestore('direct').delete_item(item.location)
|
||||
|
||||
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
|
||||
if delete_all_versions:
|
||||
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
|
||||
|
||||
for parent_loc in parent_locs:
|
||||
parent = modulestore('direct').get_item(parent_loc)
|
||||
item_url = item_loc.url()
|
||||
if item_url in parent.children:
|
||||
children = parent.children
|
||||
children.remove(item_url)
|
||||
parent.children = children
|
||||
modulestore('direct').update_children(parent.location, parent.children)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -642,7 +693,7 @@ def save_item(request):
|
||||
|
||||
# update existing metadata with submitted metadata (which can be partial)
|
||||
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
|
||||
for metadata_key in posted_metadata.keys():
|
||||
for metadata_key, value in posted_metadata.items():
|
||||
|
||||
# let's strip out any metadata fields from the postback which have been identified as system metadata
|
||||
# and therefore should not be user-editable, so we should accept them back from the client
|
||||
@@ -650,15 +701,15 @@ def save_item(request):
|
||||
del posted_metadata[metadata_key]
|
||||
elif posted_metadata[metadata_key] is None:
|
||||
# remove both from passed in collection as well as the collection read in from the modulestore
|
||||
if metadata_key in existing_item.metadata:
|
||||
del existing_item.metadata[metadata_key]
|
||||
if metadata_key in existing_item._model_data:
|
||||
del existing_item._model_data[metadata_key]
|
||||
del posted_metadata[metadata_key]
|
||||
|
||||
# overlay the new metadata over the modulestore sourced collection to support partial updates
|
||||
existing_item.metadata.update(posted_metadata)
|
||||
else:
|
||||
existing_item._model_data[metadata_key] = value
|
||||
|
||||
# commit to datastore
|
||||
store.update_metadata(item_location, existing_item.metadata)
|
||||
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
|
||||
store.update_metadata(item_location, own_metadata(existing_item))
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@@ -725,22 +776,18 @@ def clone_item(request):
|
||||
|
||||
new_item = get_modulestore(template).clone_item(template, dest_location)
|
||||
|
||||
# TODO: This needs to be deleted when we have proper storage for static content
|
||||
new_item.metadata['data_dir'] = parent.metadata['data_dir']
|
||||
|
||||
# replace the display name with an optional parameter passed in from the caller
|
||||
if display_name is not None:
|
||||
new_item.metadata['display_name'] = display_name
|
||||
new_item.display_name = display_name
|
||||
|
||||
get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
|
||||
|
||||
return HttpResponse(json.dumps({'id': dest_location.url()}))
|
||||
|
||||
#@login_required
|
||||
#@ensure_csrf_cookie
|
||||
|
||||
def upload_asset(request, org, course, coursename):
|
||||
'''
|
||||
cdodge: this method allows for POST uploading of files into the course asset library, which will
|
||||
@@ -802,6 +849,7 @@ def upload_asset(request, org, course, coursename):
|
||||
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
||||
return response
|
||||
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
@@ -834,6 +882,7 @@ def create_json_response(errmsg = None):
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
@@ -866,6 +915,7 @@ def add_user(request, location):
|
||||
|
||||
return create_json_response()
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
@@ -952,7 +1002,7 @@ def reorder_static_tabs(request):
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name': tab_items[static_tab_idx].metadata.get('display_name'),
|
||||
'name': tab_items[static_tab_idx].display_name,
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
static_tab_idx += 1
|
||||
else:
|
||||
@@ -961,7 +1011,7 @@ def reorder_static_tabs(request):
|
||||
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
course.tabs = reordered_tabs
|
||||
modulestore('direct').update_metadata(course.location, course.metadata)
|
||||
modulestore('direct').update_metadata(course.location, own_metadata(course))
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -1124,14 +1174,18 @@ def get_course_settings(request, org, course, name):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
course_details = CourseDetails.fetch(location)
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
'course_location': location,
|
||||
'details_url': reverse(course_settings_updates,
|
||||
kwargs={"org": org,
|
||||
"course": course,
|
||||
"name": name,
|
||||
"section": "details"})
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_config_graders_page(request, org, course, name):
|
||||
@@ -1156,6 +1210,29 @@ def course_config_graders_page(request, org, course, name):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_config_advanced_page(request, org, course, name):
|
||||
"""
|
||||
Send models and views as well as html for editing the advanced course settings to the client.
|
||||
|
||||
org, course, name: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
|
||||
})
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1223,6 +1300,37 @@ def course_grader_updates(request, org, course, name, grader_index=None):
|
||||
mimetype="application/json")
|
||||
|
||||
|
||||
## NB: expect_json failed on ["key", "key2"] and json payload
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def course_advanced_updates(request, org, course, name):
|
||||
"""
|
||||
restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
|
||||
the payload is either a key or a list of keys to delete.
|
||||
|
||||
org, course: Attributes of the Location for the item to edit
|
||||
"""
|
||||
location = ['i4x', org, course, 'course', name]
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
|
||||
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def asset_index(request, org, course, name):
|
||||
@@ -1324,13 +1432,10 @@ def create_new_course(request):
|
||||
new_course = modulestore('direct').clone_item(template, dest_location)
|
||||
|
||||
if display_name is not None:
|
||||
new_course.metadata['display_name'] = display_name
|
||||
|
||||
# we need a 'data_dir' for legacy reasons
|
||||
new_course.metadata['data_dir'] = uuid4().hex
|
||||
new_course.display_name = display_name
|
||||
|
||||
# set a default start date to now
|
||||
new_course.metadata['start'] = stringify_time(time.gmtime())
|
||||
new_course.start = time.gmtime()
|
||||
|
||||
initialize_course_tabs(new_course)
|
||||
|
||||
@@ -1349,12 +1454,12 @@ def initialize_course_tabs(course):
|
||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||
# so if you change anything here, you need to also change it there.
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
|
||||
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -1493,3 +1598,11 @@ def event(request):
|
||||
console logs don't get distracted :-)
|
||||
'''
|
||||
return HttpResponse(True)
|
||||
|
||||
|
||||
def render_404(request):
|
||||
return HttpResponseNotFound(render_to_string('404.html', {}))
|
||||
|
||||
|
||||
def render_500(request):
|
||||
return HttpResponseServerError(render_to_string('500.html', {}))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from xmodule.modulestore.django import modulestore
|
||||
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
|
||||
import time
|
||||
from contentstore.utils import get_modulestore
|
||||
from util.converters import jsdate_to_time, time_to_date
|
||||
from cms.djangoapps.models.settings import course_grading
|
||||
from cms.djangoapps.contentstore.utils import update_item
|
||||
from models.settings import course_grading
|
||||
from contentstore.utils import update_item
|
||||
import re
|
||||
import logging
|
||||
|
||||
@@ -43,25 +44,25 @@ class CourseDetails(object):
|
||||
|
||||
temploc = course_location._replace(category='about', name='syllabus')
|
||||
try:
|
||||
course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
course.syllabus = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='overview')
|
||||
try:
|
||||
course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
course.overview = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='effort')
|
||||
try:
|
||||
course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
course.effort = get_modulestore(temploc).get_item(temploc).data
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
|
||||
temploc = temploc._replace(name='video')
|
||||
try:
|
||||
raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
|
||||
raw_video = get_modulestore(temploc).get_item(temploc).data
|
||||
course.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
except ItemNotFoundError:
|
||||
pass
|
||||
@@ -116,7 +117,7 @@ class CourseDetails(object):
|
||||
descriptor.enrollment_end = converted
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
get_modulestore(course_location).update_metadata(course_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.
|
||||
@@ -133,7 +134,6 @@ 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
|
||||
# it persisted correctly
|
||||
return CourseDetails.fetch(course_location)
|
||||
|
||||
@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
import re
|
||||
from util import converters
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class CourseGradingModel(object):
|
||||
@@ -91,7 +92,7 @@ class CourseGradingModel(object):
|
||||
descriptor.raw_grader = graders_parsed
|
||||
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
|
||||
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
|
||||
|
||||
return CourseGradingModel.fetch(course_location)
|
||||
@@ -119,7 +120,7 @@ class CourseGradingModel(object):
|
||||
else:
|
||||
descriptor.raw_grader.append(grader)
|
||||
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
|
||||
|
||||
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
|
||||
|
||||
@@ -134,7 +135,7 @@ class CourseGradingModel(object):
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.grade_cutoffs = cutoffs
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
|
||||
|
||||
return cutoffs
|
||||
|
||||
@@ -156,11 +157,11 @@ class CourseGradingModel(object):
|
||||
graceperiodjson = graceperiodjson['grace_period']
|
||||
|
||||
# lms requires these to be in a fixed order
|
||||
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
|
||||
grace_timedelta = timedelta(**graceperiodjson)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.metadata['graceperiod'] = grace_rep
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
descriptor.lms.graceperiod = grace_timedelta
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
|
||||
|
||||
@staticmethod
|
||||
def delete_grader(course_location, index):
|
||||
@@ -176,7 +177,7 @@ class CourseGradingModel(object):
|
||||
del descriptor.raw_grader[index]
|
||||
# force propagation to definition
|
||||
descriptor.raw_grader = descriptor.raw_grader
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
|
||||
|
||||
# NOTE cannot delete cutoffs. May be useful to reset
|
||||
@staticmethod
|
||||
@@ -189,7 +190,7 @@ class CourseGradingModel(object):
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
|
||||
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
|
||||
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
|
||||
|
||||
return descriptor.grade_cutoffs
|
||||
|
||||
@@ -202,8 +203,8 @@ class CourseGradingModel(object):
|
||||
course_location = Location(course_location)
|
||||
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
|
||||
del descriptor.lms.graceperiod
|
||||
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
|
||||
|
||||
@staticmethod
|
||||
def get_section_grader_type(location):
|
||||
@@ -212,7 +213,7 @@ class CourseGradingModel(object):
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
return {
|
||||
"graderType": descriptor.metadata.get('format', u"Not Graded"),
|
||||
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
|
||||
"location": location,
|
||||
"id": 99 # just an arbitrary value to
|
||||
}
|
||||
@@ -224,23 +225,41 @@ class CourseGradingModel(object):
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
|
||||
descriptor.metadata['format'] = jsondict.get('graderType')
|
||||
descriptor.metadata['graded'] = True
|
||||
descriptor.lms.format = jsondict.get('graderType')
|
||||
descriptor.lms.graded = True
|
||||
else:
|
||||
if 'format' in descriptor.metadata: del descriptor.metadata['format']
|
||||
if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
|
||||
del descriptor.lms.format
|
||||
del descriptor.lms.graded
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor.metadata)
|
||||
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
|
||||
rawgrace = descriptor.metadata.get('graceperiod', None)
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
rawgrace = descriptor.lms.graceperiod
|
||||
if rawgrace:
|
||||
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
|
||||
return parsedgrace
|
||||
else: return None
|
||||
hours_from_days = rawgrace.days*24
|
||||
seconds = rawgrace.seconds
|
||||
hours_from_seconds = int(seconds / 3600)
|
||||
hours = hours_from_days + hours_from_seconds
|
||||
seconds -= hours_from_seconds * 3600
|
||||
minutes = int(seconds / 60)
|
||||
seconds -= minutes * 60
|
||||
|
||||
graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
|
||||
if hours > 0:
|
||||
graceperiod['hours'] = hours
|
||||
|
||||
if minutes > 0:
|
||||
graceperiod['minutes'] = minutes
|
||||
|
||||
if seconds > 0:
|
||||
graceperiod['seconds'] = seconds
|
||||
|
||||
return graceperiod
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_grader(json_grader):
|
||||
|
||||
81
cms/djangoapps/models/settings/course_metadata.py
Normal file
81
cms/djangoapps/models/settings/course_metadata.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from xmodule.modulestore import Location
|
||||
from contentstore.utils import get_modulestore
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xblock.core import Scope
|
||||
|
||||
|
||||
class CourseMetadata(object):
|
||||
'''
|
||||
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
|
||||
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
|
||||
'''
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
"""
|
||||
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)
|
||||
|
||||
for field in descriptor.fields + descriptor.lms.fields:
|
||||
if field.scope != Scope.settings:
|
||||
continue
|
||||
|
||||
if field.name not in cls.FILTERED_LIST:
|
||||
course[field.name] = field.read_from(descriptor)
|
||||
|
||||
return course
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, course_location, jsondict):
|
||||
"""
|
||||
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
|
||||
|
||||
for k, v in jsondict.iteritems():
|
||||
# should it be an error if one of the filtered list items is in the payload?
|
||||
if k in cls.FILTERED_LIST:
|
||||
continue
|
||||
|
||||
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
|
||||
dirty = True
|
||||
setattr(descriptor, k, v)
|
||||
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
|
||||
dirty = True
|
||||
setattr(descriptor.lms, k, v)
|
||||
|
||||
if dirty:
|
||||
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)
|
||||
elif hasattr(descriptor.lms, key):
|
||||
delattr(descriptor.lms, key)
|
||||
|
||||
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
|
||||
|
||||
return cls.fetch(course_location)
|
||||
@@ -62,3 +62,6 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
MODULESTORE = AUTH_TOKENS['MODULESTORE']
|
||||
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
|
||||
|
||||
# Datadog for events!
|
||||
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
@@ -20,7 +20,6 @@ Longer TODO:
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import os.path
|
||||
import os
|
||||
import lms.envs.common
|
||||
@@ -59,7 +58,8 @@ sys.path.append(COMMON_ROOT / 'lib')
|
||||
|
||||
############################# WEB CONFIGURATION #############################
|
||||
# This is where we stick our compiled template files.
|
||||
MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
|
||||
from tempdir import mkdtemp_clean
|
||||
MAKO_MODULE_DIR = mkdtemp_clean('mako')
|
||||
MAKO_TEMPLATES = {}
|
||||
MAKO_TEMPLATES['main'] = [
|
||||
PROJECT_ROOT / 'templates',
|
||||
@@ -172,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
# Tracking
|
||||
TRACK_MAX_EVENT = 10000
|
||||
|
||||
# Messages
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
@@ -275,6 +278,10 @@ INSTALLED_APPS = (
|
||||
'auth',
|
||||
'student', # misleading name due to sharing with lms
|
||||
'course_groups', # not used in cms (yet), but tests run
|
||||
|
||||
# tracking
|
||||
'track',
|
||||
|
||||
# For asset pipelining
|
||||
'pipeline',
|
||||
'staticfiles',
|
||||
|
||||
@@ -4,9 +4,6 @@ This config file runs the simplest dev environment"""
|
||||
from .common import *
|
||||
from logsettings import get_logger_config
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
@@ -99,6 +96,13 @@ CACHES = {
|
||||
'KEY_PREFIX': 'general',
|
||||
'VERSION': 4,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
},
|
||||
|
||||
'mongo_metadata_inheritance': {
|
||||
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
|
||||
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
|
||||
'TIMEOUT': 300,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,3 +111,36 @@ CACHE_TIMEOUT = 0
|
||||
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
################################ DEBUG TOOLBAR #################################
|
||||
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
|
||||
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
||||
DEBUG_TOOLBAR_PANELS = (
|
||||
'debug_toolbar.panels.version.VersionDebugPanel',
|
||||
'debug_toolbar.panels.timer.TimerDebugPanel',
|
||||
'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
|
||||
'debug_toolbar.panels.headers.HeaderDebugPanel',
|
||||
'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
|
||||
'debug_toolbar.panels.sql.SQLDebugPanel',
|
||||
'debug_toolbar.panels.signals.SignalDebugPanel',
|
||||
'debug_toolbar.panels.logger.LoggingPanel',
|
||||
# This is breaking Mongo updates-- Christina is investigating.
|
||||
# 'debug_toolbar_mongo.panel.MongoDebugPanel',
|
||||
|
||||
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
|
||||
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
|
||||
# hit twice). So you can uncomment when you need to diagnose performance
|
||||
# problems, but you shouldn't leave it on.
|
||||
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
|
||||
)
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'INTERCEPT_REDIRECTS': False
|
||||
}
|
||||
|
||||
# To see stacktraces for MongoDB queries, set this to True.
|
||||
# Stacktraces slow down page loads drastically (for pages with lots of queries).
|
||||
# DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
|
||||
@@ -27,6 +27,9 @@ STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
GITHUB_REPO_ROOT = TEST_ROOT / "data"
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
|
||||
# Makes the tests run much faster...
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
@@ -95,6 +98,13 @@ CACHES = {
|
||||
'KEY_PREFIX': 'general',
|
||||
'VERSION': 4,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
},
|
||||
|
||||
'mongo_metadata_inheritance': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': '/var/tmp/mongo_metadata_inheritance',
|
||||
'TIMEOUT': 300,
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
cms/one_time_startup.py
Normal file
14
cms/one_time_startup.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from dogapi import dog_http_api, dog_stats_api
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from django.core.cache import get_cache, InvalidCacheBackendError
|
||||
|
||||
cache = get_cache('mongo_metadata_inheritance')
|
||||
for store_name in settings.MODULESTORE:
|
||||
store = modulestore(store_name)
|
||||
store.metadata_inheritance_cache = cache
|
||||
|
||||
if hasattr(settings, 'DATADOG_API'):
|
||||
dog_http_api.api_key = settings.DATADOG_API
|
||||
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
|
||||
11
cms/static/client_templates/advanced_entry.html
Normal file
11
cms/static/client_templates/advanced_entry.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<li class="field-group course-advanced-policy-list-item">
|
||||
<div class="field is-not-editable text key" id="<%= key %>">
|
||||
<label for="<%= keyUniqueId %>">Policy Key:</label>
|
||||
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
|
||||
</div>
|
||||
|
||||
<div class="field text value">
|
||||
<label for="<%= valueUniqueId %>">Policy Value:</label>
|
||||
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"js_files": [
|
||||
"/static/js/vendor/RequireJS.js",
|
||||
"/static/js/vendor/jquery.min.js",
|
||||
"/static/js/vendor/jquery-ui.min.js",
|
||||
"/static/js/vendor/jquery.ui.draggable.js",
|
||||
"/static/js/vendor/jquery.cookie.js",
|
||||
"/static/js/vendor/json2.js",
|
||||
"/static/js/vendor/underscore-min.js",
|
||||
"/static/js/vendor/backbone-min.js"
|
||||
"static_files": [
|
||||
"js/vendor/RequireJS.js",
|
||||
"js/vendor/jquery.min.js",
|
||||
"js/vendor/jquery-ui.min.js",
|
||||
"js/vendor/jquery.ui.draggable.js",
|
||||
"js/vendor/jquery.cookie.js",
|
||||
"js/vendor/json2.js",
|
||||
"js/vendor/underscore-min.js",
|
||||
"js/vendor/backbone-min.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
class CMS.Views.TabsEdit extends Backbone.View
|
||||
events:
|
||||
'click .new-tab': 'addNewTab'
|
||||
|
||||
initialize: =>
|
||||
@$('.component').each((idx, element) =>
|
||||
@@ -13,6 +11,7 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
)
|
||||
)
|
||||
|
||||
@options.mast.find('.new-tab').on('click', @addNewTab)
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: @tabMoved
|
||||
|
||||
BIN
cms/static/img/large-advanced-icon.png
Normal file
BIN
cms/static/img/large-advanced-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 B |
BIN
cms/static/img/large-annotations-icon.png
Normal file
BIN
cms/static/img/large-annotations-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 B |
BIN
cms/static/img/large-openended-icon.png
Normal file
BIN
cms/static/img/large-openended-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 379 B |
BIN
cms/static/img/preview-lms-staticpages.png
Normal file
BIN
cms/static/img/preview-lms-staticpages.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
@@ -43,6 +43,12 @@ $(document).ready(function () {
|
||||
|
||||
$('body').addClass('js');
|
||||
|
||||
// lean/simple modal
|
||||
$('a[rel*=modal]').leanModal({overlay : 0.80, closeButton: '.action-modal-close' });
|
||||
$('a.action-modal-close').click(function(e){
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
// nav - dropdown related
|
||||
$body.click(function (e) {
|
||||
$('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown');
|
||||
@@ -638,7 +644,7 @@ function addNewCourse(e) {
|
||||
$(e.target).hide();
|
||||
var $newCourse = $($('#new-course-template').html());
|
||||
var $cancelButton = $newCourse.find('.new-course-cancel');
|
||||
$('.new-course-button').after($newCourse);
|
||||
$('.inner-wrapper').prepend($newCourse);
|
||||
$newCourse.find('.new-course-name').focus().select();
|
||||
$newCourse.find('form').bind('submit', saveNewCourse);
|
||||
$cancelButton.bind('click', cancelNewCourse);
|
||||
@@ -822,4 +828,4 @@ function saveSetSectionScheduleDate(e) {
|
||||
|
||||
hideModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
50
cms/static/js/models/settings/advanced.js
Normal file
50
cms/static/js/models/settings/advanced.js
Normal file
@@ -0,0 +1,50 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
|
||||
|
||||
CMS.Models.Settings.Advanced = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
|
||||
},
|
||||
// which keys to send as the deleted keys on next save
|
||||
deleteKeys : [],
|
||||
|
||||
validate: function (attrs) {
|
||||
// Keys can no longer be edited. We are currently not validating values.
|
||||
},
|
||||
|
||||
save : function (attrs, options) {
|
||||
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
|
||||
options = options ? _.clone(options) : {};
|
||||
// add saveSuccess to the success
|
||||
var success = options.success;
|
||||
options.success = function(model, resp, options) {
|
||||
model.afterSave(model);
|
||||
if (success) success(model, resp, options);
|
||||
};
|
||||
Backbone.Model.prototype.save.call(this, attrs, options);
|
||||
},
|
||||
|
||||
afterSave : function(self) {
|
||||
// remove deleted attrs
|
||||
if (!_.isEmpty(self.deleteKeys)) {
|
||||
// remove the to be deleted keys from the returned model
|
||||
_.each(self.deleteKeys, function(key) { self.unset(key); });
|
||||
// not able to do via backbone since we're not destroying the model
|
||||
$.ajax({
|
||||
url : self.url,
|
||||
// json to and fro
|
||||
contentType : "application/json",
|
||||
dataType : "json",
|
||||
// delete
|
||||
type : 'DELETE',
|
||||
// data
|
||||
data : JSON.stringify({ deleteKeys : self.deleteKeys})
|
||||
})
|
||||
.fail(function(hdr, status, error) { CMS.ServerError(self, "Deleting keys:" + status); })
|
||||
.done(function(data, status, error) {
|
||||
// clear deleteKeys on success
|
||||
self.deleteKeys = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -59,19 +59,14 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
|
||||
// NOTE don't return empty errors as that will be interpreted as an error state
|
||||
},
|
||||
|
||||
url: function() {
|
||||
var location = this.get('location');
|
||||
return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/details';
|
||||
},
|
||||
|
||||
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
|
||||
save_videosource: function(newsource) {
|
||||
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
|
||||
// returns the videosource for the preview which iss the key whose speed is closest to 1
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.set({'intro_video': null});
|
||||
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
|
||||
// TODO remove all whitespace w/in string
|
||||
else {
|
||||
if (this.get('intro_video') !== newsource) this.set('intro_video', newsource);
|
||||
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
|
||||
}
|
||||
|
||||
return this.videosourceSample();
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
|
||||
CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
|
||||
// a container for the models representing the n possible tabbed states
|
||||
defaults: {
|
||||
courseLocation: null,
|
||||
details: null,
|
||||
faculty: null,
|
||||
grading: null,
|
||||
problems: null,
|
||||
discussions: null
|
||||
},
|
||||
|
||||
retrieve: function(submodel, callback) {
|
||||
if (this.get(submodel)) callback();
|
||||
else {
|
||||
var cachethis = this;
|
||||
switch (submodel) {
|
||||
case 'details':
|
||||
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
|
||||
details.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('details', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'grading':
|
||||
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
|
||||
grading.fetch( {
|
||||
success : function(model) {
|
||||
cachethis.set('grading', model);
|
||||
callback(model);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -5,7 +5,7 @@
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.12",
|
||||
templateVersion: "0.0.16",
|
||||
templates: {},
|
||||
loadRemoteTemplate: function(templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
|
||||
@@ -10,7 +10,7 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
render: function() {
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
new CMS.Views.ClassInfoUpdateView({
|
||||
el: this.$('#course-update-view'),
|
||||
el: $('body.updates'),
|
||||
collection: this.model.get('updates')
|
||||
});
|
||||
|
||||
@@ -27,10 +27,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
"click .new-update-button" : "onNew",
|
||||
"click .save-button" : "onSave",
|
||||
"click .cancel-button" : "onCancel",
|
||||
"click .edit-button" : "onEdit",
|
||||
"click .delete-button" : "onDelete"
|
||||
"click #course-update-view .save-button" : "onSave",
|
||||
"click #course-update-view .cancel-button" : "onCancel",
|
||||
"click .post-actions > .edit-button" : "onEdit",
|
||||
"click .post-actions > .delete-button" : "onDelete"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
@@ -44,6 +44,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
// when the client refetches the updates as a whole, re-render them
|
||||
this.listenTo(this.collection, 'reset', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
@@ -53,8 +55,12 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
$(updateEle).empty();
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
try {
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
@@ -150,7 +156,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
closeEditor: function(self, removePost) {
|
||||
var targetModel = self.collection.getByCid(self.$currentPost.attr('name'));
|
||||
var targetModel = self.collection.get(self.$currentPost.attr('name'));
|
||||
|
||||
if(removePost) {
|
||||
self.$currentPost.remove();
|
||||
@@ -160,8 +166,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
self.$currentPost.removeClass('editing');
|
||||
self.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
self.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
|
||||
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
|
||||
try {
|
||||
// just in case the content causes an error (embedded js errors)
|
||||
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
|
||||
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
|
||||
} catch (e) {
|
||||
// ignore but handle rest of page
|
||||
}
|
||||
self.$currentPost.find('form').hide();
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
@@ -172,7 +183,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// Dereferencing from events to screen elements
|
||||
eventModel: function(event) {
|
||||
// not sure if it should be currentTarget or delegateTarget
|
||||
return this.collection.getByCid($(event.currentTarget).attr("name"));
|
||||
return this.collection.get($(event.currentTarget).attr("name"));
|
||||
},
|
||||
|
||||
modelDom: function(event) {
|
||||
|
||||
168
cms/static/js/views/settings/advanced_view.js
Normal file
168
cms/static/js/views/settings/advanced_view.js
Normal file
@@ -0,0 +1,168 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
|
||||
CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
|
||||
error_saving : "error_saving",
|
||||
successful_changes: "successful_changes",
|
||||
|
||||
// Model class is CMS.Models.Settings.Advanced
|
||||
events : {
|
||||
'focus :input' : "focusInput",
|
||||
'blur :input' : "blurInput"
|
||||
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
|
||||
},
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
// instantiates an editor template for each update in the collection
|
||||
window.templateLoader.loadRemoteTemplate("advanced_entry",
|
||||
"/static/client_templates/advanced_entry.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
// because these are outside of this.$el, they can't be in the event hash
|
||||
$('.save-button').on('click', this, this.saveView);
|
||||
$('.cancel-button').on('click', this, this.revertView);
|
||||
this.listenTo(this.model, 'error', CMS.ServerError);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
},
|
||||
render: function() {
|
||||
// catch potential outside call before template loaded
|
||||
if (!this.template) return this;
|
||||
|
||||
var listEle$ = this.$el.find('.course-advanced-policy-list');
|
||||
listEle$.empty();
|
||||
|
||||
// b/c we've deleted all old fields, clear the map and repopulate
|
||||
this.fieldToSelectorMap = {};
|
||||
this.selectorToField = {};
|
||||
|
||||
// iterate through model and produce key : value editors for each property in model.get
|
||||
var self = this;
|
||||
_.each(_.sortBy(_.keys(this.model.attributes), _.identity),
|
||||
function(key) {
|
||||
listEle$.append(self.renderTemplate(key, self.model.get(key)));
|
||||
});
|
||||
|
||||
var policyValues = listEle$.find('.json');
|
||||
_.each(policyValues, this.attachJSONEditor, this);
|
||||
this.showMessage();
|
||||
return this;
|
||||
},
|
||||
attachJSONEditor : function (textarea) {
|
||||
// Since we are allowing duplicate keys at the moment, it is possible that we will try to attach
|
||||
// JSON Editor to a value that already has one. Therefore only attach if no CodeMirror peer exists.
|
||||
if ( $(textarea).siblings().hasClass('CodeMirror')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var oldValue = $(textarea).val();
|
||||
CodeMirror.fromTextArea(textarea, {
|
||||
mode: "application/json", lineNumbers: false, lineWrapping: false,
|
||||
onChange: function(instance, changeobj) {
|
||||
// this event's being called even when there's no change :-(
|
||||
if (instance.getValue() !== oldValue) self.showSaveCancelButtons();
|
||||
},
|
||||
onFocus : function(mirror) {
|
||||
$(textarea).parent().children('label').addClass("is-focused");
|
||||
},
|
||||
onBlur: function (mirror) {
|
||||
$(textarea).parent().children('label').removeClass("is-focused");
|
||||
var key = $(mirror.getWrapperElement()).closest('.field-group').children('.key').attr('id');
|
||||
var stringValue = $.trim(mirror.getValue());
|
||||
// update CodeMirror to show the trimmed value.
|
||||
mirror.setValue(stringValue);
|
||||
var JSONValue = undefined;
|
||||
try {
|
||||
JSONValue = JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
// If it didn't parse, try converting non-arrays/non-objects to a String.
|
||||
// But don't convert single-quote strings, which are most likely errors.
|
||||
var firstNonWhite = stringValue.substring(0, 1);
|
||||
if (firstNonWhite !== "{" && firstNonWhite !== "[" && firstNonWhite !== "'") {
|
||||
try {
|
||||
stringValue = '"'+stringValue +'"';
|
||||
JSONValue = JSON.parse(stringValue);
|
||||
mirror.setValue(stringValue);
|
||||
} catch(quotedE) {
|
||||
// TODO: validation error
|
||||
// console.log("Error with JSON, even after converting to String.");
|
||||
// console.log(quotedE);
|
||||
JSONValue = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (JSONValue !== undefined) {
|
||||
self.clearValidationErrors();
|
||||
self.model.set(key, JSONValue, {validate: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
showMessage: function (type) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
if (type) {
|
||||
if (type === this.error_saving) {
|
||||
this.$el.find(".message-status.error").addClass("is-shown");
|
||||
}
|
||||
else if (type === this.successful_changes) {
|
||||
this.$el.find(".message-status.confirm").addClass("is-shown");
|
||||
this.hideSaveCancelButtons();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This is the case of the page first rendering, or when Cancel is pressed.
|
||||
this.hideSaveCancelButtons();
|
||||
}
|
||||
},
|
||||
showSaveCancelButtons: function(event) {
|
||||
if (!this.buttonsVisible) {
|
||||
this.$el.find(".message-status").removeClass("is-shown");
|
||||
$('.wrapper-notification').addClass('is-shown');
|
||||
this.buttonsVisible = true;
|
||||
}
|
||||
},
|
||||
hideSaveCancelButtons: function() {
|
||||
$('.wrapper-notification').removeClass('is-shown');
|
||||
this.buttonsVisible = false;
|
||||
},
|
||||
saveView : function(event) {
|
||||
// TODO one last verification scan:
|
||||
// call validateKey on each to ensure proper format
|
||||
// check for dupes
|
||||
var self = event.data;
|
||||
self.model.save({},
|
||||
{
|
||||
success : function() {
|
||||
self.render();
|
||||
self.showMessage(self.successful_changes);
|
||||
},
|
||||
error : CMS.ServerError
|
||||
});
|
||||
},
|
||||
revertView : function(event) {
|
||||
var self = event.data;
|
||||
self.model.deleteKeys = [];
|
||||
self.model.clear({silent : true});
|
||||
self.model.fetch({
|
||||
success : function() { self.render(); },
|
||||
error : CMS.ServerError
|
||||
});
|
||||
},
|
||||
renderTemplate: function (key, value) {
|
||||
var newKeyId = _.uniqueId('policy_key_'),
|
||||
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
|
||||
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
|
||||
|
||||
this.fieldToSelectorMap[key] = newKeyId;
|
||||
this.selectorToField[newKeyId] = key;
|
||||
return newEle;
|
||||
},
|
||||
focusInput : function(event) {
|
||||
$(event.target).prev().addClass("is-focused");
|
||||
},
|
||||
blurInput : function(event) {
|
||||
$(event.target).prev().removeClass("is-focused");
|
||||
}
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg exists
|
||||
if (!CMS.Views['Settings']) CMS.Views.Settings = {};
|
||||
|
||||
CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
'click .remove-course-syllabus' : "removeSyllabus",
|
||||
'click .new-course-syllabus' : 'assetSyllabus',
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
@@ -26,7 +26,8 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
var dateIntrospect = new Date();
|
||||
this.$el.find('#timezone').html("(" + dateIntrospect.getTimezone() + ")");
|
||||
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.listenTo(this.model, 'error', CMS.ServerError);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
@@ -97,7 +98,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
|
||||
}
|
||||
var newVal = new Date(date.getTime() + time * 1000);
|
||||
if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
|
||||
cacheModel.save(fieldName, newVal, { error: CMS.ServerError});
|
||||
cacheModel.save(fieldName, newVal);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex
|
||||
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGradingPolicy
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"blur span[contenteditable=true]" : "updateDesignation",
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
"change span[contenteditable=true]" : "updateDesignation",
|
||||
"click .settings-extra header" : "showSettingsExtras",
|
||||
"click .new-grade-button" : "addNewGrade",
|
||||
"click .remove-button" : "removeGrade",
|
||||
@@ -44,7 +44,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.listenTo(this.model, 'error', CMS.ServerError);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.model.get('graders').on('remove', this.render, this);
|
||||
this.model.get('graders').on('reset', this.render, this);
|
||||
this.model.get('graders').on('add', this.render, this);
|
||||
@@ -90,8 +91,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
setGracePeriod : function(event) {
|
||||
event.data.clearValidationErrors();
|
||||
var newVal = event.data.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime'));
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal,
|
||||
{ error : CMS.ServerError});
|
||||
if (event.data.model.get('grace_period') != newVal) event.data.model.save('grace_period', newVal);
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
@@ -227,8 +227,7 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
object[cutoff['designation']] = cutoff['cutoff'] / 100.0;
|
||||
return object;
|
||||
},
|
||||
{}),
|
||||
{ error : CMS.ServerError});
|
||||
{}));
|
||||
},
|
||||
|
||||
addNewGrade: function(e) {
|
||||
@@ -310,15 +309,16 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
|
||||
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseGrader
|
||||
events : {
|
||||
"blur input" : "updateModel",
|
||||
"blur textarea" : "updateModel",
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
"click .remove-grading-data" : "deleteModel",
|
||||
// would love to move to a general superclass, but event hashes don't inherit in backbone :-(
|
||||
'focus :input' : "inputFocus",
|
||||
'blur :input' : "inputUnfocus"
|
||||
},
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.listenTo(this.model, 'error', CMS.ServerError);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
this.render();
|
||||
},
|
||||
|
||||
@@ -3,35 +3,27 @@ CMS.Views.ValidatingView = Backbone.View.extend({
|
||||
// decorates the fields. Needs wiring per class, but this initialization shows how
|
||||
// either have your init call this one or copy the contents
|
||||
initialize : function() {
|
||||
this.model.on('error', this.handleValidationError, this);
|
||||
this.listenTo(this.model, 'error', CMS.ServerError);
|
||||
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
||||
this.selectorToField = _.invert(this.fieldToSelectorMap);
|
||||
},
|
||||
|
||||
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
|
||||
|
||||
events : {
|
||||
"blur input" : "clearValidationErrors",
|
||||
"blur textarea" : "clearValidationErrors"
|
||||
"change input" : "clearValidationErrors",
|
||||
"change textarea" : "clearValidationErrors"
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
// Your subclass must populate this w/ all of the model keys and dom selectors
|
||||
// which may be the subjects of validation errors
|
||||
},
|
||||
_cacheValidationErrors : [],
|
||||
|
||||
handleValidationError : function(model, error) {
|
||||
// error triggered either by validation or server error
|
||||
// error is object w/ fields and error strings
|
||||
for (var field in error) {
|
||||
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
|
||||
if (ele.length === 0) {
|
||||
// check if it might a server error: note a typo in the field name
|
||||
// or failure to put in a map may cause this to muffle validation errors
|
||||
if (_.has(error, 'error') && _.has(error, 'responseText')) {
|
||||
CMS.ServerError(model, error);
|
||||
return;
|
||||
}
|
||||
else continue;
|
||||
}
|
||||
this._cacheValidationErrors.push(ele);
|
||||
if ($(ele).is('div')) {
|
||||
// put error on the contained inputs
|
||||
|
||||
@@ -1,3 +1,100 @@
|
||||
// notifications
|
||||
.wrapper-notification {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
@include transition (bottom 2.0s ease-in-out 5s);
|
||||
@include box-shadow(0 -1px 2px rgba(0,0,0,0.1));
|
||||
position: fixed;
|
||||
bottom: -100px;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
border-top: 1px solid $darkGrey;
|
||||
padding: 20px 40px;
|
||||
|
||||
&.is-shown {
|
||||
bottom: 0;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.wrapper-notification-warning {
|
||||
border-color: shade($yellow, 25%);
|
||||
background: tint($yellow, 25%);
|
||||
}
|
||||
|
||||
&.wrapper-notification-error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.wrapper-notification-confirm {
|
||||
border-color: shade($green, 30%);
|
||||
background: tint($green, 40%);
|
||||
color: shade($green, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0 auto;
|
||||
width: flex-grid(12);
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(9, 12);
|
||||
margin-right: flex-gutter();
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 5px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
width: flex-grid(8, 9);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 12);
|
||||
margin-top: ($baseline/2);
|
||||
text-align: right;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// adopted alerts
|
||||
.alert {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@@ -50,7 +50,142 @@ h1 {
|
||||
|
||||
// ====================
|
||||
|
||||
// layout - basic
|
||||
// layout - basic page header
|
||||
.wrapper-mast {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
|
||||
.mast, .metadata {
|
||||
@include clearfix();
|
||||
@include font-size(16);
|
||||
position: relative;
|
||||
max-width: $fg-max-width;
|
||||
min-width: $fg-min-width;
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto $baseline auto;
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
.mast {
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
.title-sub {
|
||||
@include font-size(14);
|
||||
position: relative;
|
||||
top: ($baseline/4);
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.title, .title-1 {
|
||||
@include font-size(32);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.nav-hierarchy {
|
||||
@include font-size(14);
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: $gray-l2;
|
||||
font-weight: 400;
|
||||
|
||||
.nav-item {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
|
||||
&:after {
|
||||
content: ">>";
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
.title {
|
||||
width: flex-grid(12);
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-actions {
|
||||
@include clearfix();
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
width: flex-grid(6,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
position: relative;
|
||||
bottom: -($baseline*0.75);
|
||||
float: right;
|
||||
width: flex-grid(6,12);
|
||||
text-align: right;
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// buttons
|
||||
.button {
|
||||
padding: ($baseline/4) ($baseline/2) ($baseline/3) ($baseline/2) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.new-button {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.view-button {
|
||||
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.upload-button .icon-create {
|
||||
@include font-size(18);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// layout with actions
|
||||
&.has-subtitle {
|
||||
|
||||
.nav-actions {
|
||||
bottom: -($baseline*1.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// page metadata/action bar
|
||||
.metadata {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// layout - basic page content
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
padding: 0 $baseline;
|
||||
@@ -89,8 +224,39 @@ h1 {
|
||||
}
|
||||
|
||||
.introduction {
|
||||
@include box-sizing(border-box);
|
||||
@include font-size(14);
|
||||
width: flex-grid(12);
|
||||
margin: 0 0 $baseline 0;
|
||||
|
||||
.copy strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.has-links {
|
||||
@include clearfix();
|
||||
|
||||
.copy {
|
||||
float: left;
|
||||
width: flex-grid(8,12);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.nav-introduction-supplementary {
|
||||
@include font-size(13);
|
||||
float: right;
|
||||
width: flex-grid(4,12);
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
.icon {
|
||||
@include font-size(14);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +264,7 @@ h1 {
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
// layout - primary content
|
||||
.content-primary {
|
||||
|
||||
.title-1, .title-2, .title-3, .title-4, .title-5, .title-5 {
|
||||
@@ -129,6 +296,7 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
// layout - supplemental content
|
||||
.content-supplementary {
|
||||
|
||||
.bit {
|
||||
@@ -237,6 +405,21 @@ textarea.text {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
border-color: $gray-l4;
|
||||
color: $gray-l1;
|
||||
|
||||
&:focus {
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
@@ -494,22 +677,49 @@ hr.divide {
|
||||
|
||||
.new-button {
|
||||
@include green-button;
|
||||
font-size: 13px;
|
||||
@include font-size(13);
|
||||
padding: 8px 20px 10px;
|
||||
text-align: center;
|
||||
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-create {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/4);
|
||||
margin-top: ($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.view-button {
|
||||
@include blue-button;
|
||||
@include font-size(13);
|
||||
text-align: center;
|
||||
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-view {
|
||||
@include font-size(15);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: ($baseline/2);
|
||||
margin-top: ($baseline/5);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-button.standard,
|
||||
.delete-button.standard {
|
||||
float: left;
|
||||
@include font-size(12);
|
||||
@include white-button;
|
||||
float: left;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.edit-icon,
|
||||
@@ -579,6 +789,72 @@ hr.divide {
|
||||
|
||||
// ====================
|
||||
|
||||
// js dependant
|
||||
body.js {
|
||||
|
||||
// lean/simple modal window
|
||||
.content-modal {
|
||||
@include border-bottom-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 2px 4px $shadow-d1);
|
||||
position: relative;
|
||||
display: none;
|
||||
width: 700px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $gray-d1;
|
||||
padding: ($baseline);
|
||||
background: $white;
|
||||
|
||||
.action-modal-close {
|
||||
@include transition(top .25s ease-in-out);
|
||||
@include border-bottom-radius(3px);
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
|
||||
background: $gray-l3;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include font-size(18);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: ($baseline/10);
|
||||
border: 1px solid $gray-l4;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include font-size(18);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include font-size(13);
|
||||
margin-top: ($baseline/2);
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// works in progress
|
||||
body.hide-wip {
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
padding: 34px 0 42px;
|
||||
border-top: 1px solid #cbd1db;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
|
||||
@@ -498,6 +498,7 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix();
|
||||
@@ -506,6 +507,15 @@ input.courseware-unit-search-input {
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
padding: 25px 0 0 0;
|
||||
|
||||
.section-name {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,19 +6,27 @@
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
border-bottom: 1px solid $mediumGrey;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
.class-link {
|
||||
z-index: 100;
|
||||
display: block;
|
||||
padding: 20px 25px;
|
||||
line-height: 1.3;
|
||||
|
||||
&:hover {
|
||||
background: $paleYellow;
|
||||
|
||||
+ .view-live-button {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +42,22 @@
|
||||
margin-right: 20px;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
// view live button
|
||||
.view-live-button {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-course {
|
||||
|
||||
@@ -254,6 +254,30 @@
|
||||
background: url(../img/html-icon.png) center no-repeat;
|
||||
}
|
||||
|
||||
.large-openended-icon {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
margin-right: 5px;
|
||||
background: url(../img/large-openended-icon.png) center no-repeat;
|
||||
}
|
||||
|
||||
.large-annotations-icon {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
margin-right: 5px;
|
||||
background: url(../img/large-annotations-icon.png) center no-repeat;
|
||||
}
|
||||
|
||||
.large-advanced-icon {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
margin-right: 5px;
|
||||
background: url(../img/large-advanced-icon.png) center no-repeat;
|
||||
}
|
||||
|
||||
.large-textbook-icon {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
|
||||
@@ -350,67 +350,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// js dependant
|
||||
&.js {
|
||||
|
||||
.content-modal {
|
||||
@include border-bottom-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 2px 4px $shadow-d1);
|
||||
position: relative;
|
||||
display: none;
|
||||
width: 700px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $gray-d1;
|
||||
padding: ($baseline);
|
||||
background: $white;
|
||||
|
||||
.action-modal-close {
|
||||
@include transition(top .25s ease-in-out);
|
||||
@include border-bottom-radius(3px);
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2) 0 ($baseline/2);
|
||||
background: $gray-l3;
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
@include text-sr();
|
||||
}
|
||||
|
||||
.ss-icon {
|
||||
@include font-size(18);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $blue;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: ($baseline/10);
|
||||
border: 1px solid $gray-l4;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include font-size(18);
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
font-weight: 600;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include font-size(13);
|
||||
margin-top: ($baseline/2);
|
||||
color: $gray-l1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,44 @@ body.course.settings {
|
||||
padding: $baseline ($baseline*1.5);
|
||||
}
|
||||
|
||||
// messages - should be synced up with global messages in the future
|
||||
.message {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
border-bottom: 2px solid $yellow;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: $paleYellow;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: shade($red, 50%);
|
||||
background: tint($red, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
border-color: shade($green, 50%);
|
||||
background: tint($green, 20%);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// in form - elements
|
||||
.group-settings {
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
@@ -45,7 +83,12 @@ body.course.settings {
|
||||
|
||||
}
|
||||
|
||||
// UI hints/tips/messages
|
||||
// in form -UI hints/tips/messages
|
||||
.instructions {
|
||||
@include font-size(14);
|
||||
margin: 0 0 $baseline 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
@include font-size(13);
|
||||
@@ -196,13 +239,9 @@ body.course.settings {
|
||||
|
||||
// not editable fields
|
||||
.field.is-not-editable {
|
||||
|
||||
label, .label {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
input {
|
||||
opacity: 0.25;
|
||||
|
||||
& label.is-focused {
|
||||
color: $gray-d2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +615,119 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - advanced settings
|
||||
&.advanced-policies {
|
||||
|
||||
.field-group {
|
||||
margin-bottom: ($baseline*1.5);
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.course-advanced-policy-list-item {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
|
||||
.field {
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@include transition (opacity 0.5s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: ($baseline*1.25);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
|
||||
& + .tip {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
input.error {
|
||||
|
||||
& + .tip {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.key, .value {
|
||||
float: left;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
}
|
||||
|
||||
.key {
|
||||
width: flex-grid(3, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
.value {
|
||||
width: flex-grid(6, 9);
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: left;
|
||||
width: flex-grid(9, 9);
|
||||
|
||||
.delete-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-error {
|
||||
position: absolute;
|
||||
bottom: ($baseline*0.75);
|
||||
}
|
||||
|
||||
// specific to code mirror instance in JSON policy editing, need to sync up with other similar code mirror UIs
|
||||
.CodeMirror {
|
||||
@include font-size(16);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
padding: 5px 8px;
|
||||
border: 1px solid $mediumGrey;
|
||||
border-radius: 2px;
|
||||
background-color: $lightGrey;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
color: $baseFontColor;
|
||||
outline: 0;
|
||||
|
||||
&.CodeMirror-focused {
|
||||
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
min-height: ($baseline*1.5);
|
||||
max-height: ($baseline*10);
|
||||
}
|
||||
|
||||
// editor color changes just for JSON
|
||||
.CodeMirror-lines {
|
||||
|
||||
.cm-string {
|
||||
color: #cb9c40;
|
||||
}
|
||||
|
||||
pre {
|
||||
line-height: 2.0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
border-radius: 0;
|
||||
|
||||
&.new-component-item {
|
||||
margin-top: 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
cms/templates/404.html
Normal file
14
cms/templates/404.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Page Not Found</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
|
||||
<h1>Page not found</h1>
|
||||
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
13
cms/templates/500.html
Normal file
13
cms/templates/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Server Error</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<h1>Currently the <em>edX</em> servers are down</h1>
|
||||
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
@@ -28,17 +28,32 @@
|
||||
{{uploadDate}}
|
||||
</td>
|
||||
<td class="embed-col">
|
||||
<input type="text" class="embeddable-xml-input" value='{{url}}' disabled>
|
||||
<input type="text" class="embeddable-xml-input" value='{{url}}' readonly>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Files & Uploads</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button upload-button new-button"><i class="ss-icon ss-symbolicons-standard icon icon-create"></i> Upload New File</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="page-actions">
|
||||
<a href="#" class="upload-button new-button">
|
||||
<span class="upload-icon"></span>Upload New Asset
|
||||
</a>
|
||||
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
|
||||
</div>
|
||||
<article class="asset-library">
|
||||
@@ -69,7 +84,7 @@
|
||||
${asset['uploadDate']}
|
||||
</td>
|
||||
<td class="embed-col">
|
||||
<input type="text" class="embeddable-xml-input" value="${asset['url']}" disabled>
|
||||
<input type="text" class="embeddable-xml-input" value="${asset['url']}" readonly>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
@@ -100,7 +115,7 @@
|
||||
</div>
|
||||
<div class="embeddable">
|
||||
<label>URL:</label>
|
||||
<input type="text" class="embeddable-xml-input" value='' disabled>
|
||||
<input type="text" class="embeddable-xml-input" value='' readonly>
|
||||
</div>
|
||||
<form class="file-chooser" action="${upload_asset_callback_url}"
|
||||
method="post" enctype="multipart/form-data">
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<title>
|
||||
<%block name="title"></%block> |
|
||||
<%block name="title"></%block> |
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
${context_course.display_name} |
|
||||
${context_course.display_name_with_default} |
|
||||
% endif
|
||||
edX Studio
|
||||
</title>
|
||||
@@ -22,7 +22,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
|
||||
|
||||
|
||||
<%block name="header_extras"></%block>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<%inherit file="base.html" />
|
||||
|
||||
<%include file="widgets/header.html"/>
|
||||
|
||||
<%block name="content">
|
||||
<section class="main-container">
|
||||
|
||||
<%include file="widgets/navigation.html"/>
|
||||
|
||||
<section class="main-content">
|
||||
</section>
|
||||
|
||||
</section>
|
||||
</%block>
|
||||
@@ -20,8 +20,8 @@
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function(){
|
||||
var course_updates = new CMS.Models.CourseUpdateCollection();
|
||||
course_updates.reset(${course_updates|n});
|
||||
course_updates.urlbase = '${url_base}';
|
||||
course_updates.fetch();
|
||||
|
||||
var course_handouts = new CMS.Models.ModuleInfo({
|
||||
id: '${handouts_location}'
|
||||
@@ -42,16 +42,38 @@
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Course Updates</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class=" button new-button new-update-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Update</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p clas="copy">Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Course Info</h1>
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<h2>Course Updates & News</h2>
|
||||
<a href="#" class="new-update-button">New Update</a>
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
|
||||
@@ -9,25 +9,49 @@
|
||||
el: $('.main-wrapper'),
|
||||
model: new CMS.Models.Module({
|
||||
id: '${context_course.location}'
|
||||
})
|
||||
}),
|
||||
mast: $('.wrapper-mast')
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Static Pages</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button new-button new-tab"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Page</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction has-links">
|
||||
<p class="copy">Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.</p>
|
||||
<nav class="nav-introduction-supplementary">
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a rel="modal" href="#preview-lms-staticpages"><i class="ss-icon ss-symbolicons-block icon icon-information">❓</i>How do Static Pages look to students in my course?</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="unit-body">
|
||||
<div class="details">
|
||||
<h2>Here you can add and manage additional pages for your course</h2>
|
||||
<p>These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p>
|
||||
</div>
|
||||
|
||||
<div class="page-actions">
|
||||
<a href="#" class="new-button new-tab">
|
||||
<span class="plus-icon white"></span>New Page
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tab-list">
|
||||
<ol class='components'>
|
||||
@@ -43,4 +67,17 @@
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-modal" id="preview-lms-staticpages">
|
||||
<h3 class="title">How Static Pages are Used in Your Course</h3>
|
||||
<figure>
|
||||
<img src="/static/img/preview-lms-staticpages.png" alt="Preview of how Static Pages are used in your course" />
|
||||
<figcaption class="description">These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.</figcaption>
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block icon-close icon">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -22,7 +22,7 @@
|
||||
<article class="subsection-body window" data-id="${subsection.location}">
|
||||
<div class="subsection-name-input">
|
||||
<label>Display Name:</label>
|
||||
<input type="text" value="${subsection.display_name}" class="subsection-display-name-input" data-metadata-name="display_name"/>
|
||||
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
|
||||
</div>
|
||||
<div class="sortable-unit-list">
|
||||
<label>Units:</label>
|
||||
@@ -40,7 +40,7 @@
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
<a href="#" class="delete-icon remove-policy-data"></a>
|
||||
</li>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
@@ -51,28 +51,28 @@
|
||||
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
|
||||
<div class="datepair" data-language="javascript">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
|
||||
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
|
||||
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
|
||||
%>
|
||||
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
</div>
|
||||
% if subsection.start != parent_item.start and subsection.start:
|
||||
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
|
||||
% if parent_start_date is None:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name}, which is unset.
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
|
||||
% else:
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
|
||||
% endif
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
|
||||
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row gradable">
|
||||
<label>Graded as:</label>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="due-date-input row">
|
||||
<label>Due date:</label>
|
||||
<a href="#" class="set-date">Set a due date</a>
|
||||
@@ -80,9 +80,9 @@
|
||||
<p class="date-description">
|
||||
<%
|
||||
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
|
||||
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
|
||||
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
|
||||
%>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
|
||||
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
|
||||
<a href="#" class="remove-date">Remove due date</a>
|
||||
</p>
|
||||
@@ -110,7 +110,7 @@
|
||||
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
@@ -128,7 +128,7 @@
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
}
|
||||
|
||||
|
||||
$(".gradable-status").each(function(index, ele) {
|
||||
var gradeView = new CMS.Views.OverviewAssignmentGrader({
|
||||
el : ele,
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
<%block name="bodyclass">is-signedin course tools export</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Tools</span>
|
||||
<h1 class="title-1">Course Export</h1>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="export-overview">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<img src="/static/img/thumb-hiw-feature1.png" alt="Studio Helps You Keep Your Courses Organized" />
|
||||
<figcaption class="sr">Studio Helps You Keep Your Courses Organized</figcaption>
|
||||
<span class="action-zoom">
|
||||
<i class="ss-icon ss-symbolicons-block"></i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-zoom"></i>
|
||||
</span>
|
||||
</a>
|
||||
</figure>
|
||||
@@ -62,7 +62,7 @@
|
||||
<img src="/static/img/thumb-hiw-feature2.png" alt="Learning is More than Just Lectures" />
|
||||
<figcaption class="sr">Learning is More than Just Lectures</figcaption>
|
||||
<span class="action-zoom">
|
||||
<i class="ss-icon ss-symbolicons-block"></i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-zoom"></i>
|
||||
</span>
|
||||
</a>
|
||||
</figure>
|
||||
@@ -96,7 +96,7 @@
|
||||
<img src="/static/img/thumb-hiw-feature3.png" alt="Studio Gives You Simple, Fast, and Incremental Publishing. With Friends." />
|
||||
<figcaption class="sr">Studio Gives You Simple, Fast, and Incremental Publishing. With Friends.</figcaption>
|
||||
<span class="action-zoom">
|
||||
<i class="ss-icon ss-symbolicons-block"></i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-zoom"></i>
|
||||
</span>
|
||||
</a>
|
||||
</figure>
|
||||
@@ -152,7 +152,7 @@
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block">␡</i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block">␡</i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -178,22 +178,8 @@
|
||||
</figure>
|
||||
|
||||
<a href="#" rel="view" class="action action-modal-close">
|
||||
<i class="ss-icon ss-symbolicons-block">␡</i>
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-close">␡</i>
|
||||
<span class="label">close modal</span>
|
||||
</a>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
|
||||
// lean modal window
|
||||
$('a[rel*=modal]').leanModal({overlay : 0.50, closeButton: '.action-modal-close' });
|
||||
$('a.action-modal-close').click(function(e){
|
||||
(e).preventDefault();
|
||||
});
|
||||
|
||||
})(this)
|
||||
</script>
|
||||
</%block>
|
||||
@@ -6,21 +6,29 @@
|
||||
<%block name="bodyclass">is-signedin course tools import</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Tools</span>
|
||||
<h1 class="title-1">Course Import</h1>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="import-overview">
|
||||
<div class="description">
|
||||
<h2>Please <a href="https://edge.edx.org/courses/edX/edx101/edX_Studio_Reference/about" target="_blank">read the documentation</a> before attempting an import!</h2>
|
||||
<p><strong>Importing a new course will delete all content currently associated with your course
|
||||
and replace it with the contents of the uploaded file.</strong></p>
|
||||
<p>File uploads must be gzipped tar files (.tar.gz or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
|
||||
<p>File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a <code>course.xml</code> file.</p>
|
||||
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
|
||||
re-importing your course could cause the loss of student data associated with those problems.</p>
|
||||
</div>
|
||||
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
|
||||
<h2>Course to import:</h2>
|
||||
<p class="error-block"></p>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
|
||||
<input type="file" name="course-data" class="file-input">
|
||||
<input type="submit" value="Replace my course with the one above" class="submit-button">
|
||||
@@ -37,13 +45,13 @@
|
||||
<%block name="jsextra">
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
|
||||
var bar = $('.progress-bar');
|
||||
var fill = $('.progress-fill');
|
||||
var percent = $('.percent');
|
||||
var status = $('#status');
|
||||
var submitBtn = $('.submit-button');
|
||||
|
||||
|
||||
$('form').ajaxForm({
|
||||
beforeSend: function() {
|
||||
status.empty();
|
||||
@@ -68,7 +76,7 @@ $('form').ajaxForm({
|
||||
submitBtn.show();
|
||||
bar.hide();
|
||||
}
|
||||
});
|
||||
})();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</%block>
|
||||
@@ -33,35 +33,57 @@
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>My Courses</h1>
|
||||
<article class="my-classes">
|
||||
% if user.is_active:
|
||||
% if not disable_course_creation:
|
||||
<a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a>
|
||||
%endif
|
||||
<ul class="class-list">
|
||||
%for course, url in courses:
|
||||
<li>
|
||||
<a href="${url}" class="class-name">
|
||||
<span class="class-name">${course}</span>
|
||||
<!--
|
||||
<span class="detail">Started: 9/21/2012</span>
|
||||
<span class="detail">Ends: 10/21/2012</span>
|
||||
-->
|
||||
</a>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
% else:
|
||||
<div class='warn-msg'>
|
||||
<p>
|
||||
In order to start authoring courses using edX studio, please click on the activation link in your email.
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions">
|
||||
<div class="title">
|
||||
<h1 class="title-1">My Courses</h1>
|
||||
</div>
|
||||
|
||||
% if user.is_active:
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
% if not disable_course_creation:
|
||||
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Course</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<div class="introduction">
|
||||
<p class="copy"><strong>Welcome, ${ user.username }</strong>. Here are all of the courses you are currently authoring in Studio:</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<article class="my-classes">
|
||||
% if user.is_active:
|
||||
<ul class="class-list">
|
||||
%for course, url, lms_link in courses:
|
||||
<li>
|
||||
<a class="class-link" href="${url}" class="class-name">
|
||||
<span class="class-name">${course}</span>
|
||||
</a>
|
||||
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view"></i>View Live</a>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
% else:
|
||||
<div class='warn-msg'>
|
||||
<p>
|
||||
In order to start authoring courses using edX Studio, please click on the activation link in your email.
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -4,15 +4,28 @@
|
||||
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Settings</span>
|
||||
<h1 class="title-1">Course Team</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
%if allow_actions:
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button new-button new-user-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New User</a>
|
||||
</li>
|
||||
%endif
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="page-actions">
|
||||
%if allow_actions:
|
||||
<a href="#" class="new-button new-user-button">
|
||||
<span class="plus-icon white"></span>New User
|
||||
</a>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div>${module_type}</div>
|
||||
<div>
|
||||
% for template in module_templates:
|
||||
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
|
||||
<a class="save" data-template-id="${template.location.url()}">${template.display_name_with_default}</a>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
|
||||
window.graderTypes.reset(${course_graders|n});
|
||||
}
|
||||
|
||||
|
||||
$(".gradable-status").each(function(index, ele) {
|
||||
var gradeView = new CMS.Views.OverviewAssignmentGrader({
|
||||
el : ele,
|
||||
@@ -40,7 +40,7 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -120,13 +120,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Course Content</span>
|
||||
<h1 class="title-1">Course Outline</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
<ul>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block icon">up</i> <span class="label">Collapse All Sections</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="button new-button new-courseware-section-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">+</i> New Section</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view"></i>View Live</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="page-actions">
|
||||
<a href="#" class="new-button new-courseware-section-button"><span class="plus-icon white"></span> New Section</a>
|
||||
<a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span></a>
|
||||
</div>
|
||||
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
|
||||
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
|
||||
% for section in sections:
|
||||
<section class="courseware-section branch" data-id="${section.location}">
|
||||
<header>
|
||||
@@ -134,16 +154,16 @@
|
||||
|
||||
<div class="item-details" data-id="${section.location}">
|
||||
<h3 class="section-name">
|
||||
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
|
||||
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name_with_default}</span>
|
||||
<form class="section-name-edit" style="display:none">
|
||||
<input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/>
|
||||
<input type="text" value="${section.display_name_with_default | h}" class="edit-section-name" autocomplete="off"/>
|
||||
<input type="submit" class="save-button edit-section-name-save" value="Save" />
|
||||
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
|
||||
</form>
|
||||
</h3>
|
||||
<div class="section-published-date">
|
||||
<%
|
||||
start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
|
||||
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
|
||||
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
|
||||
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
|
||||
%>
|
||||
@@ -154,9 +174,9 @@
|
||||
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
|
||||
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="item-actions">
|
||||
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
|
||||
@@ -176,15 +196,15 @@
|
||||
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
|
||||
<a href="${reverse('edit_subsection', args=[subsection.location])}">
|
||||
<span class="folder-icon"></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
|
||||
|
||||
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="item-actions">
|
||||
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,36 +23,42 @@ from contentstore import utils
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
|
||||
// hilighting labels when fields are focused in
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
}).blur(function() {
|
||||
$("label").removeClass("is-focused");
|
||||
});
|
||||
|
||||
var editor = new CMS.Views.Settings.Details({
|
||||
el: $('.settings-details'),
|
||||
model: new CMS.Models.Settings.CourseDetails(${course_details|n},{parse:true})
|
||||
});
|
||||
|
||||
editor.render();
|
||||
var model = new CMS.Models.Settings.CourseDetails();
|
||||
model.urlRoot = '${details_url}';
|
||||
model.fetch({success :
|
||||
function(model) {
|
||||
var editor = new CMS.Views.Settings.Details({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
});
|
||||
|
||||
editor.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header class="page">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Settings</span>
|
||||
<h1 class="title-1">Schedule & Details</h1>
|
||||
</header>
|
||||
|
||||
<!-- <div class="introduction">
|
||||
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_details" class="settings-details" method="post" action="">
|
||||
<section class="group-settings basic">
|
||||
@@ -64,17 +70,17 @@ from contentstore import utils
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
<label for="course-organization">Organization</label>
|
||||
<input type="text" class="long" id="course-organization" value="[Course Organization]" disabled="disabled" />
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-number">
|
||||
<label for="course-number">Course Number</label>
|
||||
<input type="text" class="short" id="course-number" value="[Course No.]" disabled="disabled">
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-name">
|
||||
<label for="course-name">Course Name</label>
|
||||
<input type="text" class="long" id="course-name" value="[Course Name]" disabled="disabled" />
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
|
||||
@@ -205,7 +211,7 @@ from contentstore import utils
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used</h3>
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
|
||||
|
||||
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
|
||||
@@ -220,6 +226,7 @@ from contentstore import utils
|
||||
<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="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">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>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
116
cms/templates/settings_advanced.html
Normal file
116
cms/templates/settings_advanced.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="title">Advanced Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course advanced settings</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
<%block name="jsextra">
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
|
||||
var advancedModel = new CMS.Models.Settings.Advanced(${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))}";
|
||||
|
||||
var editor = new CMS.Views.Settings.Advanced({
|
||||
el: $('.settings-advanced'),
|
||||
model: advancedModel
|
||||
});
|
||||
|
||||
editor.render();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header class="page">
|
||||
<span class="title-sub">Settings</span>
|
||||
<h1 class="title-1">Advanced Settings</h1>
|
||||
</header>
|
||||
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_advanced" class="settings-advanced" method="post" action="">
|
||||
|
||||
<div class="message message-status confirm">
|
||||
Your policy changes have been saved.
|
||||
</div>
|
||||
|
||||
<div class="message message-status error">
|
||||
There was an error saving your information. Please see below.
|
||||
</div>
|
||||
|
||||
<section class="group-settings advanced-policies">
|
||||
<header>
|
||||
<h2 class="title-2">Manual Policy Definition</h2>
|
||||
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
|
||||
</header>
|
||||
|
||||
<p class="instructions"><strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.</p>
|
||||
|
||||
<ul class="list-input course-advanced-policy-list enum">
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Manual policies are JSON-based key and value pairs that give you control over specific course settings that edX Studio will use when displaying and running your course.</p>
|
||||
|
||||
<p>Any policies you modify here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not edit policies that you are unfamiliar with (both their purpose and their syntax).</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<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 & 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="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- notification: change has been made and a save is needed -->
|
||||
<div class="wrapper wrapper-notification wrapper-notification-warning">
|
||||
<div class="notification warning">
|
||||
<div class="copy">
|
||||
<i class="ss-icon ss-symbolicons-block icon icon-warning">⚠</i>
|
||||
|
||||
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
|
||||
progress</strong>. Take care with policy value formatting, as validation is <strong>not implemented</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<ul>
|
||||
<li><a href="#" class="save-button">Save</a></li>
|
||||
<li><a href="#" class="cancel-button">Cancel</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -39,17 +39,17 @@ from contentstore import utils
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<header class="page">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<div class="title">
|
||||
<span class="title-sub">Settings</span>
|
||||
<h1 class="title-1">Grading</h1>
|
||||
</header>
|
||||
|
||||
<!-- <div class="introduction">
|
||||
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_details" class="settings-grading" method="post" action="">
|
||||
<section class="group-settings grade-range">
|
||||
@@ -126,7 +126,7 @@ from contentstore import utils
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used</h3>
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Your grading settings will be used to calculate students grades and performance.</p>
|
||||
|
||||
<p>Overall grade range will be used in students' final grades, which are calculated by the weighting you determine for each custom assignment type.</p>
|
||||
@@ -141,6 +141,7 @@ from contentstore import utils
|
||||
<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 & Schedule</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">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>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
</div>
|
||||
<div class="main-column">
|
||||
<article class="unit-body window">
|
||||
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name}" class="unit-display-name-input" /></p>
|
||||
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name_with_default | h}" class="unit-display-name-input" /></p>
|
||||
<ol class="components">
|
||||
% for id in components:
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
<li class="new-component-item adding">
|
||||
<li class="new-component-item adding">
|
||||
<div class="new-component">
|
||||
<h5>Add New Component</h5>
|
||||
<ul class="new-component-type">
|
||||
@@ -86,7 +86,7 @@
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
% else:
|
||||
<li class="editor-md">
|
||||
<a href="#" data-location="${location}">
|
||||
@@ -95,7 +95,7 @@
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
@@ -103,20 +103,20 @@
|
||||
<div class="tab" id="tab2">
|
||||
<ul class="new-component-template">
|
||||
% for name, location, has_markdown, is_empty in templates:
|
||||
% if not has_markdown:
|
||||
% if not has_markdown:
|
||||
% if is_empty:
|
||||
<li class="editor-manual empty">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
% else:
|
||||
<li class="editor-manual">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
@@ -147,13 +147,13 @@
|
||||
<div class="row published-alert">
|
||||
<p class="edit-draft-message">This unit has been published. To make changes, you must <a href="#" class="create-draft">edit a draft</a>.</p>
|
||||
<p class="publish-draft-message">This is a draft of the published unit. To update the live version, you must <a href="#" class="publish-draft">replace it with this draft</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row status">
|
||||
<p>This unit is scheduled to be released to <strong>students</strong>
|
||||
<p>This unit is scheduled to be released to <strong>students</strong>
|
||||
% if release_date is not None:
|
||||
on <strong>${release_date}</strong>
|
||||
% endif
|
||||
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
|
||||
% endif
|
||||
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name_with_default}"</a></p>
|
||||
</div>
|
||||
<div class="row unit-actions">
|
||||
<a href="#" class="delete-draft delete-button">Delete Draft</a>
|
||||
@@ -168,18 +168,18 @@
|
||||
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#" class="section-item">${section.display_name}</a>
|
||||
<a href="#" class="section-item">${section.display_name_with_default}</a>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
|
||||
<span class="folder-icon"></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</a>
|
||||
${units.enum_units(subsection, actions=False, selected=unit.location)}
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,24 +5,24 @@
|
||||
|
||||
<div class="wrapper wrapper-left ">
|
||||
<h1 class="branding"><a href="/">edX Studio</a></h1>
|
||||
|
||||
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<div class="info-course">
|
||||
<h2 class="sr">Current Course:</h2>
|
||||
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
|
||||
<span class="course-org">${ctx_loc.org}</span><span class="course-number">${ctx_loc.course}</span>
|
||||
<span class="course-title" title="${context_course.display_name}">${context_course.display_name}</span>
|
||||
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<nav class="nav-course primary nav-dropdown" role="navigation">
|
||||
<h2 class="sr">PH207x's Navigation:</h2>
|
||||
<h2 class="sr">${context_course.display_name_with_default}'s Navigation:</h2>
|
||||
|
||||
<ol>
|
||||
<li class="nav-item nav-course-courseware">
|
||||
<h3 class="title"><span class="label-prefix">Course </span>Content <i class="ss-icon ss-symbolicons-block icon-expand">▾</i></h3>
|
||||
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
@@ -32,27 +32,27 @@
|
||||
<li class="nav-item nav-course-courseware-uploads"><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Files & Uploads</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-course-settings">
|
||||
<h3 class="title"><span class="label-prefix">Course </span>Settings <i class="ss-icon ss-symbolicons-block icon-expand">▾</i></h3>
|
||||
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
<li class="nav-item nav-course-settings-schedule"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Schedule & Details</a></li>
|
||||
<li class="nav-item nav-course-settings-grading"><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 nav-course-settings-team"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
<!-- <li class="nav-item nav-course-settings-advanced"><a href="${reverse('course_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li> -->
|
||||
<li class="nav-item nav-course-settings-advanced"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-course-tools">
|
||||
<h3 class="title">Tools <i class="ss-icon ss-symbolicons-block icon-expand">▾</i></h3>
|
||||
|
||||
|
||||
<div class="wrapper wrapper-nav-sub">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
@@ -66,16 +66,16 @@
|
||||
</nav>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
||||
<div class="wrapper wrapper-right">
|
||||
% if user.is_authenticated():
|
||||
% if user.is_authenticated():
|
||||
<nav class="nav-account nav-is-signedin nav-dropdown">
|
||||
<h2 class="sr">Currently logged in as:</h2>
|
||||
<ol>
|
||||
<li class="nav-item nav-account-username">
|
||||
<a href="#" class="title">
|
||||
<span class="account-username">
|
||||
<i class="ss-icon ss-symbolicons-standard icon-user">👤</i>
|
||||
<i class="ss-icon ss-symbolicons-standard icon-user">👤</i>
|
||||
${ user.username }
|
||||
</span>
|
||||
<i class="ss-icon ss-symbolicons-block icon-expand">▾</i>
|
||||
@@ -111,7 +111,7 @@
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
% if metadata:
|
||||
<%
|
||||
import hashlib
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
<ul>
|
||||
% for keyname in editable_metadata_fields:
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if keyname=='source_code':
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% else:
|
||||
<label>${keyname}:</label>
|
||||
<input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' />
|
||||
% endif
|
||||
% if field_name == 'source_code':
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% else:
|
||||
<label>${field_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_name}' value='${field_value}' size='60' />
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
@@ -22,4 +21,3 @@
|
||||
% endif
|
||||
|
||||
</section>
|
||||
% endif
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<section class="cal">
|
||||
<header class="wip">
|
||||
<ul class="actions">
|
||||
<li><a href="#">Timeline view</a></li>
|
||||
<li><a href="#">Multi-Module edit</a></li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<h2>Sort:</h2>
|
||||
<select>
|
||||
<option value="">Linear Order</option>
|
||||
<option value="">Recently Modified</option>
|
||||
<option value="">Type</option>
|
||||
<option value="">Alphabetically</option>
|
||||
</select>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h2>Filter:</h2>
|
||||
<select>
|
||||
<option value="">All content</option>
|
||||
<option value="">Videos</option>
|
||||
<option value="">Problems</option>
|
||||
<option value="">Labs</option>
|
||||
<option value="">Tutorials</option>
|
||||
<option value="">HTML</option>
|
||||
</select>
|
||||
<a href="#" class="more">More</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">Hide goals</a>
|
||||
</li>
|
||||
<li class="search">
|
||||
<input type="search" name="" id="" value="" placeholder="Search" />
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<ol id="weeks">
|
||||
% for week in weeks:
|
||||
<li class="week" data-id="${week.location.url()}">
|
||||
<header>
|
||||
<h1><a href="#" class="week-edit">${week.url_name}</a></h1>
|
||||
<ul>
|
||||
% if 'goals' in week.metadata:
|
||||
% for goal in week.metadata['goals']:
|
||||
<li class="goal editable">${goal}</li>
|
||||
% endfor
|
||||
% else:
|
||||
<li class="goal editable">Please create a learning goal for this week</li>
|
||||
% endif
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<ul class="modules">
|
||||
% for module in week.get_children():
|
||||
<li class="module"
|
||||
data-id="${module.location.url()}"
|
||||
data-type="${module.js_module_name}"
|
||||
data-preview-type="${module.module_class.js_module_name}">
|
||||
|
||||
<a href="#" class="module-edit">${module.display_name}</a>
|
||||
</li>
|
||||
% endfor
|
||||
<%include file="module-dropdown.html"/>
|
||||
</ul>
|
||||
</li>
|
||||
%endfor
|
||||
</ol>
|
||||
|
||||
<section class="new-section">
|
||||
<a href="#" class="wip" >+ Add New Section</a>
|
||||
|
||||
<section class="hidden">
|
||||
<form>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="text" name="" id="" placeholder="Section title" />
|
||||
</li>
|
||||
<li>
|
||||
<select>
|
||||
<option>Blank</option>
|
||||
<option>6.002x</option>
|
||||
<option>6.00x</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Save and edit week" class="edit-week" />
|
||||
|
||||
<div>
|
||||
<a href="#" class="close">Save without edit</a>
|
||||
<a href="#" class="close">cancel</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<a href="#" class="module-edit"
|
||||
data-id="${child.location.url()}"
|
||||
data-type="${child.js_module_name}"
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
|
||||
data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
%endfor
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2>High Level Source Editing</h2>
|
||||
</header>
|
||||
|
||||
<form id="hls-form">
|
||||
<form id="hls-form" enctype="multipart/form-data">
|
||||
<section class="source-edit">
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
|
||||
</section>
|
||||
@@ -18,6 +18,9 @@
|
||||
<button type="reset" class="hls-compile">Save & Compile to edX XML</button>
|
||||
<button type="reset" class="hls-save">Save</button>
|
||||
<button type="reset" class="hls-refresh">Refresh</button>
|
||||
<div style="display:none" id="hls-finput"></div>
|
||||
<span id="message"></span>
|
||||
<button type="reset" class="hls-upload" style="float:right">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -25,88 +28,148 @@
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="/static/js/vendor/CodeMirror/stex.js"></script>
|
||||
<script type="text/javascript">
|
||||
$('#hls-trig-${hlskey}').leanModal({ top:40, overlay:0.8, closeButton: ".close-button"});
|
||||
<script type = "text/javascript">
|
||||
hlstrig = $('#hls-trig-${hlskey}');
|
||||
hlsmodal = $('#hls-modal-${hlskey}');
|
||||
|
||||
$('#hls-modal-${hlskey}').data('editor',CodeMirror.fromTextArea($('#hls-modal-${hlskey}').find('.hls-data')[0], {lineNumbers: true, mode: 'stex'}));
|
||||
hlstrig.leanModal({
|
||||
top: 40,
|
||||
overlay: 0.8,
|
||||
closeButton: ".close-button"
|
||||
});
|
||||
|
||||
$('#hls-trig-${hlskey}').click(function(){slow_refresh_hls($('#hls-modal-${hlskey}'))})
|
||||
hlsmodal.data('editor', CodeMirror.fromTextArea(hlsmodal.find('.hls-data')[0], {
|
||||
lineNumbers: true,
|
||||
mode: 'stex'
|
||||
}));
|
||||
|
||||
// refresh button
|
||||
$('#hls-trig-${hlskey}').click(function() {
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-refresh').click(function(){refresh_hls($('#hls-modal-${hlskey}'))});
|
||||
## this ought to be done using css instead
|
||||
hlsmodal.attr('style', function(i,s) { return s + ' margin-left:0px !important; left:5%' });
|
||||
|
||||
function refresh_hls(el){
|
||||
el.data('editor').refresh();
|
||||
}
|
||||
## setup file input
|
||||
## need to insert this only after hls triggered, because otherwise it
|
||||
## causes other <form> elements to become multipart/form-data,
|
||||
## thus breaking multiple-choice input forms, for example.
|
||||
$('#hls-finput').append('<input type="file" name="hlsfile" id="hlsfile" />');
|
||||
|
||||
function slow_refresh_hls(el){
|
||||
el.delay(200).queue(function(){
|
||||
refresh_hls(el);
|
||||
$(this).dequeue();
|
||||
});
|
||||
// resize the codemirror box
|
||||
h = el.height();
|
||||
el.find('.CodeMirror-scroll').height(h-100);
|
||||
}
|
||||
var el = $('#hls-modal-${hlskey}');
|
||||
setup_autoupload(el);
|
||||
slow_refresh_hls(el);
|
||||
});
|
||||
|
||||
// compile & save button
|
||||
// file upload button
|
||||
hlsmodal.find('.hls-upload').click(function() {
|
||||
$('#hls-modal-${hlskey}').find('#hlsfile').trigger('click');
|
||||
});
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-compile').click(compile_hls_${hlskey});
|
||||
|
||||
function compile_hls_${hlskey}(){
|
||||
// auto-upload after file is chosen
|
||||
function setup_autoupload(el){
|
||||
el.find('#hlsfile').change(function() {
|
||||
var file = el.find('#hlsfile')[0].files[0];
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
var contents = event.target.result;
|
||||
el.data('editor').setValue(contents);
|
||||
// trigger compile & save
|
||||
el.find('.hls-compile').trigger('click');
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
editor = $('#hls-modal-${hlskey}').data('editor')
|
||||
var myquery = { latexin: editor.getValue() };
|
||||
// refresh button
|
||||
hlsmodal.find('.hls-refresh').click(function() {
|
||||
refresh_hls(hlsmodal);
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '${metadata.get('source_processor_url','https://qisx.mit.edu:5443/latex2edx')}',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
data: escape(JSON.stringify(myquery)),
|
||||
crossDomain: true,
|
||||
dataType: 'jsonp',
|
||||
jsonpCallback: 'process_return_${hlskey}',
|
||||
beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); },
|
||||
timeout : 7000,
|
||||
success: function(result) {
|
||||
console.log(result);
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: cannot connect to latex2edx server');
|
||||
console.log('error!');
|
||||
function refresh_hls(el) {
|
||||
el.data('editor').refresh();
|
||||
$('#lean_overlay').hide(); // hide gray overlay
|
||||
}
|
||||
|
||||
function slow_refresh_hls(el) {
|
||||
el.delay(200).queue(function() {
|
||||
refresh_hls(el);
|
||||
$(this).dequeue();
|
||||
});
|
||||
// resize the codemirror box
|
||||
var h = el.height();
|
||||
el.find('.CodeMirror-scroll').height(h - 100);
|
||||
}
|
||||
|
||||
// compile & save button
|
||||
hlsmodal.find('.hls-compile').click(function() {
|
||||
var el = $('#hls-modal-${hlskey}');
|
||||
compile_hls(el);
|
||||
});
|
||||
|
||||
// connect to server using POST (requires cross-domain-access)
|
||||
|
||||
function compile_hls(el) {
|
||||
var editor = el.data('editor')
|
||||
var hlsdata = editor.getValue();
|
||||
|
||||
$.ajax({
|
||||
url: "https://studio-input-filter.mitx.mit.edu/latex2edx?raw=1",
|
||||
type: "POST",
|
||||
data: "" + hlsdata,
|
||||
crossDomain: true,
|
||||
processData: false,
|
||||
success: function(data) {
|
||||
xml = data.xml;
|
||||
if (xml.length == 0) {
|
||||
alert('Conversion failed! error:' + data.message);
|
||||
} else {
|
||||
el.closest('.component').find('.CodeMirror-wrap')[0].CodeMirror.setValue(xml);
|
||||
save_hls(el);
|
||||
}
|
||||
});
|
||||
// $('#hls-modal-${hlskey}').hide();
|
||||
}
|
||||
|
||||
function process_return_${hlskey}(datadict){
|
||||
// datadict is json of array with "xml" and "message"
|
||||
// if "xml" value is '' then the conversion failed
|
||||
xml = datadict.xml;
|
||||
console.log('xml:');
|
||||
console.log(xml);
|
||||
if (xml.length==0){
|
||||
alert('Conversion failed! error:'+ datadict.message);
|
||||
}else{
|
||||
set_raw_edit_box(xml,'${hlskey}');
|
||||
save_hls($('#hls-modal-${hlskey}'));
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: cannot connect to latex2edx server');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function process_return_${hlskey}(datadict) {
|
||||
// datadict is json of array with "xml" and "message"
|
||||
// if "xml" value is '' then the conversion failed
|
||||
var xml = datadict.xml;
|
||||
if (xml.length == 0) {
|
||||
alert('Conversion failed! error:' + datadict.message);
|
||||
} else {
|
||||
set_raw_edit_box(xml, '${hlskey}');
|
||||
save_hls($('#hls-modal-${hlskey}'));
|
||||
}
|
||||
}
|
||||
|
||||
function set_raw_edit_box(data,key){
|
||||
// get the codemirror editor for the raw-edit-box
|
||||
// it's a CodeMirror-wrap class element
|
||||
$('#hls-modal-'+key).closest('.component').find('.CodeMirror-wrap')[0].CodeMirror.setValue(data);
|
||||
}
|
||||
|
||||
// save button
|
||||
function set_raw_edit_box(data, key) {
|
||||
// get the codemirror editor for the raw-edit-box
|
||||
// it's a CodeMirror-wrap class element
|
||||
$('#hls-modal-' + key).closest('.component').find('.CodeMirror-wrap')[0].CodeMirror.setValue(data);
|
||||
}
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-save').click(function(){save_hls($('#hls-modal-${hlskey}'))});
|
||||
|
||||
function save_hls(el){
|
||||
el.find('.hls-data').val(el.data('editor').getValue());
|
||||
el.closest('.component').find('.save-button').click();
|
||||
}
|
||||
// save button
|
||||
|
||||
hlsmodal.find('.hls-save').click(function() {
|
||||
save_hls(hlsmodal);
|
||||
});
|
||||
|
||||
function save_hls(el) {
|
||||
el.find('.hls-data').val(el.data('editor').getValue());
|
||||
el.closest('.component').find('.save-button').click();
|
||||
}
|
||||
|
||||
## add upload and download links / buttons to component edit box
|
||||
hlsmodal.closest('.component').find('.component-actions').append('<div id="link-${hlskey}" style="float:right;"></div>');
|
||||
$('#link-${hlskey}').html('<a class="upload-button standard" id="upload-${hlskey}">upload</a>');
|
||||
$('#upload-${hlskey}').click(function() {
|
||||
hlsmodal.closest('.component').find('.edit-button').trigger('click'); // open up editor window
|
||||
$('#hls-trig-${hlskey}').trigger('click'); // open up HLS editor window
|
||||
hlsmodal.find('#hlsfile').trigger('click');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
<div class="section-item ${selected_class}">
|
||||
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
|
||||
<span class="${unit.category}-icon"></span>
|
||||
<span class="unit-name">${unit.display_name}</span>
|
||||
<span class="unit-name">${unit.display_name_with_default}</span>
|
||||
</a>
|
||||
% if actions:
|
||||
<div class="item-actions">
|
||||
@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</%def>
|
||||
</%def>
|
||||
|
||||
|
||||
|
||||
|
||||
11
cms/urls.py
11
cms/urls.py
@@ -1,5 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
from . import one_time_startup
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
@@ -47,6 +48,10 @@ urlpatterns = ('',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
|
||||
# This is the URL to initially render the course advanced settings.
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)$', 'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
|
||||
# This is the URL used by BackBone for updating and re-fetching the model.
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)/update.*$', 'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
|
||||
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
|
||||
|
||||
@@ -99,3 +104,9 @@ if settings.ENABLE_JASMINE:
|
||||
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
#Custom error pages
|
||||
handler404 = 'contentstore.views.render_404'
|
||||
handler500 = 'contentstore.views.render_500'
|
||||
|
||||
|
||||
|
||||
46
cms/xmodule_namespace.py
Normal file
46
cms/xmodule_namespace.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Namespace defining common fields used by Studio for all blocks
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from xblock.core import Namespace, Boolean, Scope, ModelType, String
|
||||
|
||||
|
||||
class StringyBoolean(Boolean):
|
||||
"""
|
||||
Reads strings from JSON as booleans.
|
||||
|
||||
If the string is 'true' (case insensitive), then return True,
|
||||
otherwise False.
|
||||
|
||||
JSON values that aren't strings are returned as is
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if isinstance(value, basestring):
|
||||
return value.lower() == 'true'
|
||||
return value
|
||||
|
||||
|
||||
class DateTuple(ModelType):
|
||||
"""
|
||||
ModelType that stores datetime objects as time tuples
|
||||
"""
|
||||
def from_json(self, value):
|
||||
return datetime.datetime(*value[0:6])
|
||||
|
||||
def to_json(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return list(value.timetuple())
|
||||
|
||||
|
||||
class CmsNamespace(Namespace):
|
||||
"""
|
||||
Namespace with fields common to all blocks in Studio
|
||||
"""
|
||||
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
@@ -6,6 +6,7 @@ forums, and to the cohort admin views.
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import Http404
|
||||
import logging
|
||||
import random
|
||||
|
||||
from courseware import courses
|
||||
from student.models import get_user_by_username_or_email
|
||||
@@ -65,6 +66,22 @@ def is_commentable_cohorted(course_id, commentable_id):
|
||||
return ans
|
||||
|
||||
|
||||
def get_cohorted_commentables(course_id):
|
||||
"""
|
||||
Given a course_id return a list of strings representing cohorted commentables
|
||||
"""
|
||||
|
||||
course = courses.get_course_by_id(course_id)
|
||||
|
||||
if not course.is_cohorted:
|
||||
# this is the easy case :)
|
||||
ans = []
|
||||
else:
|
||||
ans = course.cohorted_discussions
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
def get_cohort(user, course_id):
|
||||
"""
|
||||
Given a django User and a course_id, return the user's cohort in that
|
||||
@@ -96,9 +113,38 @@ def get_cohort(user, course_id):
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
users__id=user.id)
|
||||
except CourseUserGroup.DoesNotExist:
|
||||
# TODO: add auto-cohorting logic here once we know what that will be.
|
||||
# Didn't find the group. We'll go on to create one if needed.
|
||||
pass
|
||||
|
||||
if not course.auto_cohort:
|
||||
return None
|
||||
|
||||
choices = course.auto_cohort_groups
|
||||
n = len(choices)
|
||||
if n == 0:
|
||||
# Nowhere to put user
|
||||
log.warning("Course %s is auto-cohorted, but there are no"
|
||||
" auto_cohort_groups specified",
|
||||
course_id)
|
||||
return None
|
||||
|
||||
# Put user in a random group, creating it if needed
|
||||
choice = random.randrange(0, n)
|
||||
group_name = choices[choice]
|
||||
|
||||
# Victor: we are seeing very strange behavior on prod, where almost all users
|
||||
# end up in the same group. Log at INFO to try to figure out what's going on.
|
||||
log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
|
||||
user, group_name,choice))
|
||||
|
||||
group, created = CourseUserGroup.objects.get_or_create(
|
||||
course_id=course_id,
|
||||
group_type=CourseUserGroup.COHORT,
|
||||
name=group_name)
|
||||
|
||||
user.course_groups.add(group)
|
||||
return group
|
||||
|
||||
|
||||
def get_course_cohorts(course_id):
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
|
||||
|
||||
from course_groups.models import CourseUserGroup
|
||||
from course_groups.cohorts import (get_cohort, get_course_cohorts,
|
||||
is_commentable_cohorted)
|
||||
is_commentable_cohorted, get_cohort_by_name)
|
||||
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
|
||||
@@ -47,7 +47,10 @@ class TestCohorts(django.test.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def config_course_cohorts(course, discussions,
|
||||
cohorted, cohorted_discussions=None):
|
||||
cohorted,
|
||||
cohorted_discussions=None,
|
||||
auto_cohort=None,
|
||||
auto_cohort_groups=None):
|
||||
"""
|
||||
Given a course with no discussion set up, add the discussions and set
|
||||
the cohort config appropriately.
|
||||
@@ -59,6 +62,9 @@ class TestCohorts(django.test.TestCase):
|
||||
cohorted: bool.
|
||||
cohorted_discussions: optional list of topic names. If specified,
|
||||
converts them to use the same ids as topic names.
|
||||
auto_cohort: optional bool.
|
||||
auto_cohort_groups: optional list of strings
|
||||
(names of groups to put students into).
|
||||
|
||||
Returns:
|
||||
Nothing -- modifies course in place.
|
||||
@@ -70,13 +76,19 @@ class TestCohorts(django.test.TestCase):
|
||||
"id": to_id(name)})
|
||||
for name in discussions)
|
||||
|
||||
course.metadata["discussion_topics"] = topics
|
||||
course.discussion_topics = topics
|
||||
|
||||
d = {"cohorted": cohorted}
|
||||
if cohorted_discussions is not None:
|
||||
d["cohorted_discussions"] = [to_id(name)
|
||||
for name in cohorted_discussions]
|
||||
course.metadata["cohort_config"] = d
|
||||
|
||||
if auto_cohort is not None:
|
||||
d["auto_cohort"] = auto_cohort
|
||||
if auto_cohort_groups is not None:
|
||||
d["auto_cohort_groups"] = auto_cohort_groups
|
||||
|
||||
course.cohort_config = d
|
||||
|
||||
|
||||
def setUp(self):
|
||||
@@ -89,12 +101,9 @@ class TestCohorts(django.test.TestCase):
|
||||
|
||||
|
||||
def test_get_cohort(self):
|
||||
# Need to fix this, but after we're testing on staging. (Looks like
|
||||
# problem is that when get_cohort internally tries to look up the
|
||||
# course.id, it fails, even though we loaded it through the modulestore.
|
||||
|
||||
# Proper fix: give all tests a standard modulestore that uses the test
|
||||
# dir.
|
||||
"""
|
||||
Make sure get_cohort() does the right thing when the course is cohorted
|
||||
"""
|
||||
course = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.assertEqual(course.id, "edX/toy/2012_Fall")
|
||||
self.assertFalse(course.is_cohorted)
|
||||
@@ -122,6 +131,85 @@ class TestCohorts(django.test.TestCase):
|
||||
self.assertEquals(get_cohort(other_user, course.id), None,
|
||||
"other_user shouldn't have a cohort")
|
||||
|
||||
def test_auto_cohorting(self):
|
||||
"""
|
||||
Make sure get_cohort() does the right thing when the course is auto_cohorted
|
||||
"""
|
||||
course = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.assertEqual(course.id, "edX/toy/2012_Fall")
|
||||
self.assertFalse(course.is_cohorted)
|
||||
|
||||
user1 = User.objects.create(username="test", email="a@b.com")
|
||||
user2 = User.objects.create(username="test2", email="a2@b.com")
|
||||
user3 = User.objects.create(username="test3", email="a3@b.com")
|
||||
|
||||
cohort = CourseUserGroup.objects.create(name="TestCohort",
|
||||
course_id=course.id,
|
||||
group_type=CourseUserGroup.COHORT)
|
||||
|
||||
# user1 manually added to a cohort
|
||||
cohort.users.add(user1)
|
||||
|
||||
# Make the course auto cohorted...
|
||||
self.config_course_cohorts(course, [], cohorted=True,
|
||||
auto_cohort=True,
|
||||
auto_cohort_groups=["AutoGroup"])
|
||||
|
||||
self.assertEquals(get_cohort(user1, course.id).id, cohort.id,
|
||||
"user1 should stay put")
|
||||
|
||||
self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
|
||||
"user2 should be auto-cohorted")
|
||||
|
||||
# Now make the group list empty
|
||||
self.config_course_cohorts(course, [], cohorted=True,
|
||||
auto_cohort=True,
|
||||
auto_cohort_groups=[])
|
||||
|
||||
self.assertEquals(get_cohort(user3, course.id), None,
|
||||
"No groups->no auto-cohorting")
|
||||
|
||||
# Now make it different
|
||||
self.config_course_cohorts(course, [], cohorted=True,
|
||||
auto_cohort=True,
|
||||
auto_cohort_groups=["OtherGroup"])
|
||||
|
||||
self.assertEquals(get_cohort(user3, course.id).name, "OtherGroup",
|
||||
"New list->new group")
|
||||
self.assertEquals(get_cohort(user2, course.id).name, "AutoGroup",
|
||||
"user2 should still be in originally placed cohort")
|
||||
|
||||
|
||||
def test_auto_cohorting_randomization(self):
|
||||
"""
|
||||
Make sure get_cohort() randomizes properly.
|
||||
"""
|
||||
course = modulestore().get_course("edX/toy/2012_Fall")
|
||||
self.assertEqual(course.id, "edX/toy/2012_Fall")
|
||||
self.assertFalse(course.is_cohorted)
|
||||
|
||||
groups = ["group_{0}".format(n) for n in range(5)]
|
||||
self.config_course_cohorts(course, [], cohorted=True,
|
||||
auto_cohort=True,
|
||||
auto_cohort_groups=groups)
|
||||
|
||||
# Assign 100 users to cohorts
|
||||
for i in range(100):
|
||||
user = User.objects.create(username="test_{0}".format(i),
|
||||
email="a@b{0}.com".format(i))
|
||||
get_cohort(user, course.id)
|
||||
|
||||
# Now make sure that the assignment was at least vaguely random:
|
||||
# each cohort should have at least 1, and fewer than 50 students.
|
||||
# (with 5 groups, probability of 0 users in any group is about
|
||||
# .8**100= 2.0e-10)
|
||||
for cohort_name in groups:
|
||||
cohort = get_cohort_by_name(course.id, cohort_name)
|
||||
num_users = cohort.users.count()
|
||||
self.assertGreater(num_users, 1)
|
||||
self.assertLess(num_users, 50)
|
||||
|
||||
|
||||
|
||||
def test_get_course_cohorts(self):
|
||||
course1_id = 'a/b/c'
|
||||
|
||||
@@ -2,8 +2,9 @@ import json
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from dogapi import dog_stats_api
|
||||
|
||||
|
||||
@dog_stats_api.timed('edxapp.heartbeat')
|
||||
def heartbeat(request):
|
||||
"""
|
||||
Simple view that a loadbalancer can check to verify that the app is up
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.template.loaders.app_directories import Loader as AppDirectoriesLoad
|
||||
from mitxmako.template import Template
|
||||
import mitxmako.middleware
|
||||
|
||||
import tempdir
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,7 +31,7 @@ class MakoLoader(object):
|
||||
|
||||
if module_directory is None:
|
||||
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
|
||||
module_directory = tempfile.mkdtemp()
|
||||
module_directory = tempdir.mkdtemp_clean()
|
||||
|
||||
self.module_directory = module_directory
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
import tempfile
|
||||
import tempdir
|
||||
from django.template import RequestContext
|
||||
from django.conf import settings
|
||||
|
||||
@@ -29,7 +29,7 @@ class MakoMiddleware(object):
|
||||
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
|
||||
|
||||
if module_directory is None:
|
||||
module_directory = tempfile.mkdtemp()
|
||||
module_directory = tempdir.mkdtemp_clean()
|
||||
|
||||
for location in template_locations:
|
||||
lookup[location] = TemplateLookup(directories=template_locations[location],
|
||||
|
||||
@@ -84,12 +84,19 @@ def replace_static_urls(text, data_directory, course_namespace=None):
|
||||
if rest.endswith('?raw'):
|
||||
return original
|
||||
|
||||
# course_namespace is not None, then use studio style urls
|
||||
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
|
||||
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
|
||||
# In debug mode, if we can find the url as is,
|
||||
elif settings.DEBUG and finders.find(rest, True):
|
||||
if settings.DEBUG and finders.find(rest, True):
|
||||
return original
|
||||
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
|
||||
elif course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
|
||||
# first look in the static file pipeline and see if we are trying to reference
|
||||
# a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule)
|
||||
if staticfiles_storage.exists(rest):
|
||||
url = staticfiles_storage.url(rest)
|
||||
else:
|
||||
# if not, then assume it's courseware specific content and then look in the
|
||||
# Mongo-backed database
|
||||
url = StaticContent.convert_legacy_static_url(rest, course_namespace)
|
||||
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
|
||||
else:
|
||||
course_path = "/".join((data_directory, rest))
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
from django.test.utils import override_settings
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from status import get_site_status_msg
|
||||
from .status import get_site_status_msg
|
||||
|
||||
# Get a name where we can put test files
|
||||
TMP_FILE = NamedTemporaryFile(delete=False)
|
||||
|
||||
@@ -10,6 +10,7 @@ import paramiko
|
||||
import boto
|
||||
|
||||
dog_http_api.api_key = settings.DATADOG_API
|
||||
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
import os
|
||||
from tempfile import mkdtemp
|
||||
import cStringIO
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from django.test import TestCase
|
||||
@@ -143,23 +144,18 @@ class PearsonTestCase(TestCase):
|
||||
'''
|
||||
Base class for tests running Pearson-related commands
|
||||
'''
|
||||
import_dir = mkdtemp(prefix="import")
|
||||
export_dir = mkdtemp(prefix="export")
|
||||
|
||||
def assertErrorContains(self, error_message, expected):
|
||||
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
|
||||
|
||||
def setUp(self):
|
||||
self.import_dir = mkdtemp(prefix="import")
|
||||
self.addCleanup(shutil.rmtree, self.import_dir)
|
||||
self.export_dir = mkdtemp(prefix="export")
|
||||
self.addCleanup(shutil.rmtree, self.export_dir)
|
||||
|
||||
def tearDown(self):
|
||||
def delete_temp_dir(dirname):
|
||||
if os.path.exists(dirname):
|
||||
for filename in os.listdir(dirname):
|
||||
os.remove(os.path.join(dirname, filename))
|
||||
os.rmdir(dirname)
|
||||
|
||||
# clean up after any test data was dumped to temp directory
|
||||
delete_temp_dir(self.import_dir)
|
||||
delete_temp_dir(self.export_dir)
|
||||
|
||||
pass
|
||||
# and clean up the database:
|
||||
# TestCenterUser.objects.all().delete()
|
||||
# TestCenterRegistration.objects.all().delete()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user