Commit 00472956 by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into feature/cdodge/add-test-delete

parents 816b0edf 7fa77769
...@@ -27,4 +27,5 @@ lms/lib/comment_client/python ...@@ -27,4 +27,5 @@ lms/lib/comment_client/python
nosetests.xml nosetests.xml
cover_html/ cover_html/
.idea/ .idea/
.redcar/
chromedriver.log chromedriver.log
\ No newline at end of file
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 only display_name on a newly created course
Given I have opened a new course in Studio
When I select the Advanced Settings
Then I see only the display name
Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
Then there are no advanced policy settings
And I reload the page
Then there are no advanced policy settings
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Cancel" notification button
Then the policy key name is unchanged
Scenario: Test editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Save" notification button
Then the policy key name is changed
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
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
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page
Then they are alphabetized
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object
Then it is displayed as formatted
from lettuce import world, step
from common import *
import time
from nose.tools import assert_equal
from nose.tools import assert_true
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
"""
from selenium.webdriver.common.keys import Keys
############### 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')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.fill('new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
world.browser.click_link_by_text(name)
@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 :)
"""
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}')
click_save()
############### RESULTS ####################
@step('I see only the display name$')
def i_see_only_display_name(step):
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('there are no advanced policy settings$')
def no_policy_settings(step):
assert_policy_entries([], [])
@step('they are alphabetized$')
def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'new')
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course"')
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course X"')
############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium has a bug that fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = '.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
delete_buttons[index].click()
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input', expected_keys)
assert_entries('.json', expected_values)
def assert_entries(css, expected_values):
webElements = css_find(css)
assert_equal(len(expected_values), len(webElements))
# Sometimes get stale reference if I hold on to the array of elements
for counter in range(len(expected_values)):
assert_equal(expected_values[counter], css_find(css)[counter].value)
def click_save():
css = ".save-button"
def is_shown(driver):
visible = css_find(css).first.visible
if visible:
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
time.sleep(float(1))
return visible
wait_for(is_shown)
css_click(css)
def fill_last_field(value):
newValue = css_find('#__new_advanced_key__ input').first
newValue.fill(value)
from lettuce import world, step from lettuce import world, step
from factories import *
from django.core.management import call_command
from lettuce.django import django_url 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_true
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory
import xmodule.modulestore.django import xmodule.modulestore.django
from auth.authz import get_user_by_email
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -44,6 +45,13 @@ def i_press_the_category_delete_icon(step, category): ...@@ -44,6 +45,13 @@ def i_press_the_category_delete_icon(step, category):
assert False, 'Invalid category: %s' % category assert False, 'Invalid category: %s' % category
css_click(css) 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 ############## ####### HELPER FUNCTIONS ##############
...@@ -86,13 +94,38 @@ def assert_css_with_text(css, text): ...@@ -86,13 +94,38 @@ def assert_css_with_text(css, text):
def css_click(css): def css_click(css):
assert_true(world.browser.is_element_present_by_css(css, 5))
world.browser.find_by_css(css).first.click() world.browser.find_by_css(css).first.click()
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
'''
assert_true(world.browser.is_element_present_by_css(css, 5))
e = world.browser.find_by_css(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): def css_fill(css, value):
world.browser.find_by_css(css).first.fill(value) world.browser.find_by_css(css).first.fill(value)
def css_find(css):
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 10).until(func)
def id_find(id):
return world.browser.find_by_id(id)
def clear_courses(): def clear_courses():
flush_xmodule_store() flush_xmodule_store()
...@@ -129,9 +162,18 @@ def log_into_studio( ...@@ -129,9 +162,18 @@ def log_into_studio(
def create_a_course(): def create_a_course():
css_click('a.new-course-button') c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
fill_in_course_info()
css_click('input.new-course-save') # 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' course_title_css = 'span.course-title'
assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
......
...@@ -4,13 +4,6 @@ from common import * ...@@ -4,13 +4,6 @@ from common import *
############### ACTIONS #################### ############### 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$') @step('I click the new section link$')
def i_click_new_section_link(step): def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
...@@ -46,6 +39,7 @@ def i_save_a_new_section_release_date(step): ...@@ -46,6 +39,7 @@ def i_save_a_new_section_release_date(step):
css_fill(time_css, '12:00am') css_fill(time_css, '12:00am')
css_click('a.save-button') css_click('a.save-button')
############ ASSERTIONS ################### ############ ASSERTIONS ###################
......
import datetime import datetime
import time
import json import json
import calendar
import copy import copy
from util import converters from util import converters
from util.converters import jsdate_to_time from util.converters import jsdate_to_time
...@@ -11,7 +9,6 @@ from django.test.client import Client ...@@ -11,7 +9,6 @@ from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
import xmodule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails, from cms.djangoapps.models.settings.course_details import (CourseDetails,
CourseSettingsEncoder) CourseSettingsEncoder)
...@@ -22,6 +19,10 @@ from django.test import TestCase ...@@ -22,6 +19,10 @@ from django.test import TestCase
from utils import ModuleStoreTestCase from utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from cms.djangoapps.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 # YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase): class ConvertersTestCase(TestCase):
...@@ -261,3 +262,64 @@ class CourseGradingTest(CourseTestCase): ...@@ -261,3 +262,64 @@ class CourseGradingTest(CourseTestCase):
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1 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]) 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") 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,
{ "a" : 1,
"b_a_c_h" : { "c" : "test" },
"test_text" : "a text string"})
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,
{ "a" : 2,
"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('a', test_model, 'Missing revised a metadata field')
self.assertEqual(test_model['a'], 2, "a 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('a', test_model, 'Missing new a metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text 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.assertNotIn('showanswer', test_model, 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
\ No newline at end of file
...@@ -75,12 +75,15 @@ def get_course_for_item(location): ...@@ -75,12 +75,15 @@ def get_course_for_item(location):
return courses[0] 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: if settings.LMS_BASE is not None:
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
preview='preview.' if preview else '', preview='preview.' if preview else '',
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
course_id=get_course_id(location), course_id= course_id,
location=Location(location) location=Location(location)
) )
else: else:
......
...@@ -58,8 +58,8 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\ ...@@ -58,8 +58,8 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseSettingsEncoder CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore from cms.djangoapps.contentstore.utils import get_modulestore
from lxml import etree
from django.shortcuts import redirect from django.shortcuts import redirect
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
...@@ -114,7 +114,7 @@ def index(request): ...@@ -114,7 +114,7 @@ def index(request):
""" """
List all courses available to the logged in user 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 # filter out courses that we don't have access too
def course_filter(course): def course_filter(course):
...@@ -132,7 +132,7 @@ def index(request): ...@@ -132,7 +132,7 @@ def index(request):
course.location.org, course.location.org,
course.location.course, course.location.course,
course.location.name]), course.location.name]),
get_lms_link_for_item(course.location)) get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses], for course in courses],
'user': request.user, 'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
...@@ -365,7 +365,6 @@ def preview_component(request, location): ...@@ -365,7 +365,6 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -682,7 +681,6 @@ def create_draft(request): ...@@ -682,7 +681,6 @@ def create_draft(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def publish_draft(request): def publish_draft(request):
...@@ -712,7 +710,6 @@ def unpublish_unit(request): ...@@ -712,7 +710,6 @@ def unpublish_unit(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def clone_item(request): def clone_item(request):
...@@ -901,7 +898,6 @@ def remove_user(request, location): ...@@ -901,7 +898,6 @@ def remove_user(request, location):
def landing(request, org, course, coursename): def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {}) return render_to_response('temp-course-landing.html', {})
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def static_pages(request, org, course, coursename): def static_pages(request, org, course, coursename):
...@@ -1005,7 +1001,6 @@ def edit_tabs(request, org, course, coursename): ...@@ -1005,7 +1001,6 @@ def edit_tabs(request, org, course, coursename):
'components': components 'components': components
}) })
def not_found(request): def not_found(request):
return render_to_response('error.html', {'error': '404'}) return render_to_response('error.html', {'error': '404'})
...@@ -1041,7 +1036,6 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -1041,7 +1036,6 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1112,7 +1106,6 @@ def module_info(request, module_location): ...@@ -1112,7 +1106,6 @@ def module_info(request, module_location):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def get_course_settings(request, org, course, name): def get_course_settings(request, org, course, name):
...@@ -1159,6 +1152,28 @@ def course_config_graders_page(request, org, course, name): ...@@ -1159,6 +1152,28 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@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_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@expect_json @expect_json
@login_required @login_required
...@@ -1191,7 +1206,6 @@ def course_settings_updates(request, org, course, name, section): ...@@ -1191,7 +1206,6 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1226,6 +1240,37 @@ def course_grader_updates(request, org, course, name, grader_index=None): ...@@ -1226,6 +1240,37 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)), return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
mimetype="application/json") 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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1286,7 +1331,6 @@ def asset_index(request, org, course, name): ...@@ -1286,7 +1331,6 @@ def asset_index(request, org, course, name):
def edge(request): def edge(request):
return render_to_response('university_profiles/edge.html', {}) return render_to_response('university_profiles/edge.html', {})
@login_required @login_required
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
...@@ -1342,7 +1386,6 @@ def create_new_course(request): ...@@ -1342,7 +1386,6 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()})) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course): def initialize_course_tabs(course):
# set up the default tabs # set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
...@@ -1360,7 +1403,6 @@ def initialize_course_tabs(course): ...@@ -1360,7 +1403,6 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), course.own_metadata) modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
...@@ -1438,7 +1480,6 @@ def import_course(request, org, course, name): ...@@ -1438,7 +1480,6 @@ def import_course(request, org, course, name):
course_module.location.name]) course_module.location.name])
}) })
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def generate_export_course(request, org, course, name): def generate_export_course(request, org, course, name):
...@@ -1490,7 +1531,6 @@ def export_course(request, org, course, name): ...@@ -1490,7 +1531,6 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': '' 'successful_import_redirect_url': ''
}) })
def event(request): def event(request):
''' '''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
......
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
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.
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@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 k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
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 not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
dirty = True
descriptor.metadata[k] = v
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
# 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 key in descriptor.metadata:
del descriptor.metadata[key]
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return cls.fetch(course_location)
\ No newline at end of file
...@@ -172,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi ...@@ -172,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
# Tracking
TRACK_MAX_EVENT = 10000
# Messages # Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
...@@ -275,6 +278,10 @@ INSTALLED_APPS = ( ...@@ -275,6 +278,10 @@ INSTALLED_APPS = (
'auth', 'auth',
'student', # misleading name due to sharing with lms 'student', # misleading name due to sharing with lms
'course_groups', # not used in cms (yet), but tests run 'course_groups', # not used in cms (yet), but tests run
# tracking
'track',
# For asset pipelining # For asset pipelining
'pipeline', 'pipeline',
'staticfiles', 'staticfiles',
......
<li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>
\ No newline at end of file
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
// the key for a newly added policy-- before the user has entered a key value
new_key : "__new_advanced_key__",
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 : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
var errors = {};
for (var key in attrs) {
if (key === this.new_key || _.isEmpty(key)) {
errors[key] = "A key must be entered.";
}
else if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
},
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 = [];
});
}
}
});
...@@ -68,10 +68,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -68,10 +68,10 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
save_videosource: function(newsource) { save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string // 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 // 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 // TODO remove all whitespace w/in string
else { 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(); return this.videosourceSample();
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.12", templateVersion: "0.0.15",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) { if (!this.templates[templateName]) {
......
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({ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseDetails // Model class is CMS.Models.Settings.CourseDetails
events : { events : {
"blur input" : "updateModel", "change input" : "updateModel",
"blur textarea" : "updateModel", "change textarea" : "updateModel",
'click .remove-course-syllabus' : "removeSyllabus", 'click .remove-course-syllabus' : "removeSyllabus",
'click .new-course-syllabus' : 'assetSyllabus', 'click .new-course-syllabus' : 'assetSyllabus',
'click .remove-course-introduction-video' : "removeVideo", 'click .remove-course-introduction-video' : "removeVideo",
......
...@@ -3,9 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex ...@@ -3,9 +3,9 @@ if (!CMS.Views['Settings']) CMS.Views.Settings = {}; // ensure the pseudo pkg ex
CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGradingPolicy // Model class is CMS.Models.Settings.CourseGradingPolicy
events : { events : {
"blur input" : "updateModel", "change input" : "updateModel",
"blur textarea" : "updateModel", "change textarea" : "updateModel",
"blur span[contenteditable=true]" : "updateDesignation", "change span[contenteditable=true]" : "updateDesignation",
"click .settings-extra header" : "showSettingsExtras", "click .settings-extra header" : "showSettingsExtras",
"click .new-grade-button" : "addNewGrade", "click .new-grade-button" : "addNewGrade",
"click .remove-button" : "removeGrade", "click .remove-button" : "removeGrade",
...@@ -310,8 +310,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ ...@@ -310,8 +310,8 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({
CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({ CMS.Views.Settings.GraderView = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseGrader // Model class is CMS.Models.Settings.CourseGrader
events : { events : {
"blur input" : "updateModel", "change input" : "updateModel",
"blur textarea" : "updateModel", "change textarea" : "updateModel",
"click .remove-grading-data" : "deleteModel", "click .remove-grading-data" : "deleteModel",
// would love to move to a general superclass, but event hashes don't inherit in backbone :-( // would love to move to a general superclass, but event hashes don't inherit in backbone :-(
'focus :input' : "inputFocus", 'focus :input' : "inputFocus",
......
...@@ -10,8 +10,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -10,8 +10,8 @@ CMS.Views.ValidatingView = Backbone.View.extend({
errorTemplate : _.template('<span class="message-error"><%= message %></span>'), errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
events : { events : {
"blur input" : "clearValidationErrors", "change input" : "clearValidationErrors",
"blur textarea" : "clearValidationErrors" "change textarea" : "clearValidationErrors"
}, },
fieldToSelectorMap : { fieldToSelectorMap : {
// Your subclass must populate this w/ all of the model keys and dom selectors // Your subclass must populate this w/ all of the model keys and dom selectors
......
// 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 { .alert {
padding: 15px 20px; padding: 15px 20px;
margin-bottom: 30px; margin-bottom: 30px;
......
...@@ -14,6 +14,44 @@ body.course.settings { ...@@ -14,6 +14,44 @@ body.course.settings {
padding: $baseline ($baseline*1.5); 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 { .group-settings {
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
...@@ -45,7 +83,12 @@ body.course.settings { ...@@ -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 { .tip {
@include transition(color, 0.15s, ease-in-out); @include transition(color, 0.15s, ease-in-out);
@include font-size(13); @include font-size(13);
...@@ -576,6 +619,119 @@ body.course.settings { ...@@ -576,6 +619,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 { .content-supplementary {
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<article class="subsection-body window" data-id="${subsection.location}"> <article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input"> <div class="subsection-name-input">
<label>Display Name:</label> <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 | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div> </div>
<div class="sortable-unit-list"> <div class="sortable-unit-list">
<label>Units:</label> <label>Units:</label>
......
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
<div class="inner-wrapper"> <div class="inner-wrapper">
<article class="import-overview"> <article class="import-overview">
<div class="description"> <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 <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> 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 or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
......
...@@ -156,7 +156,7 @@ ...@@ -156,7 +156,7 @@
<h3 class="section-name"> <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}</span>
<form class="section-name-edit" style="display:none"> <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 | h}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" /> <input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" /> <input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form> </form>
......
...@@ -23,7 +23,8 @@ from contentstore import utils ...@@ -23,7 +23,8 @@ from contentstore import utils
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function(){ $(document).ready(function(){
// hilighting labels when fields are focused in
$("form :input").focus(function() { $("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused"); $("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() { }).blur(function() {
...@@ -205,7 +206,7 @@ from contentstore import utils ...@@ -205,7 +206,7 @@ from contentstore import utils
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <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>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> <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 +221,7 @@ from contentstore import utils ...@@ -220,6 +221,7 @@ from contentstore import utils
<ul> <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('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('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> </ul>
</nav> </nav>
% endif % endif
......
<%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.blacklistKeys = ${advanced_blacklist | n};
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>: Add only manual policy data that you are familiar
with.</p>
<ul class="list-input course-advanced-policy-list enum">
</ul>
<div class="actions">
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</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 allow you add additional settings which edX Studio will use when generating your course.</p>
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add 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 &amp; Schedule</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
<li class="nav-item"><a href="${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">&#x26A0;</i>
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
progress</strong>. Take care with key and 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>
\ No newline at end of file
...@@ -126,7 +126,7 @@ from contentstore import utils ...@@ -126,7 +126,7 @@ from contentstore import utils
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit"> <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>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> <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 ...@@ -141,6 +141,7 @@ from contentstore import utils
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li> <li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</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> </ul>
</nav> </nav>
% endif % endif
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
</div> </div>
<div class="main-column"> <div class="main-column">
<article class="unit-body window"> <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 | h}" class="unit-display-name-input" /></p>
<ol class="components"> <ol class="components">
% for id in components: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<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 &amp; Details</a></li> <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 &amp; 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-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-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> </ul>
</div> </div>
</div> </div>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<h2>High Level Source Editing</h2> <h2>High Level Source Editing</h2>
</header> </header>
<form id="hls-form"> <form id="hls-form" enctype="multipart/form-data">
<section class="source-edit"> <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> <textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
</section> </section>
...@@ -18,6 +18,9 @@ ...@@ -18,6 +18,9 @@
<button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button> <button type="reset" class="hls-compile">Save &amp; Compile to edX XML</button>
<button type="reset" class="hls-save">Save</button> <button type="reset" class="hls-save">Save</button>
<button type="reset" class="hls-refresh">Refresh</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> </div>
</form> </form>
...@@ -25,88 +28,148 @@ ...@@ -25,88 +28,148 @@
</section> </section>
<script type="text/javascript" src="/static/js/vendor/CodeMirror/stex.js"></script> <script type="text/javascript" src="/static/js/vendor/CodeMirror/stex.js"></script>
<script type="text/javascript"> <script type = "text/javascript">
$('#hls-trig-${hlskey}').leanModal({ top:40, overlay:0.8, closeButton: ".close-button"}); 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({
$('#hls-trig-${hlskey}').click(function(){slow_refresh_hls($('#hls-modal-${hlskey}'))}) top: 40,
overlay: 0.8,
// refresh button closeButton: ".close-button"
});
$('#hls-modal-${hlskey}').find('.hls-refresh').click(function(){refresh_hls($('#hls-modal-${hlskey}'))});
hlsmodal.data('editor', CodeMirror.fromTextArea(hlsmodal.find('.hls-data')[0], {
function refresh_hls(el){ lineNumbers: true,
el.data('editor').refresh(); mode: 'stex'
} }));
function slow_refresh_hls(el){ $('#hls-trig-${hlskey}').click(function() {
el.delay(200).queue(function(){
refresh_hls(el); ## this ought to be done using css instead
$(this).dequeue(); hlsmodal.attr('style', function(i,s) { return s + ' margin-left:0px !important; left:5%' });
});
// resize the codemirror box ## setup file input
h = el.height(); ## need to insert this only after hls triggered, because otherwise it
el.find('.CodeMirror-scroll').height(h-100); ## 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" />');
// compile & save button
var el = $('#hls-modal-${hlskey}');
$('#hls-modal-${hlskey}').find('.hls-compile').click(compile_hls_${hlskey}); setup_autoupload(el);
slow_refresh_hls(el);
function compile_hls_${hlskey}(){ });
editor = $('#hls-modal-${hlskey}').data('editor') // file upload button
var myquery = { latexin: editor.getValue() }; hlsmodal.find('.hls-upload').click(function() {
$('#hls-modal-${hlskey}').find('#hlsfile').trigger('click');
$.ajax({ });
url: '${metadata.get('source_processor_url','https://qisx.mit.edu:5443/latex2edx')}',
type: 'GET', // auto-upload after file is chosen
contentType: 'application/json', function setup_autoupload(el){
data: escape(JSON.stringify(myquery)), el.find('#hlsfile').change(function() {
crossDomain: true, var file = el.find('#hlsfile')[0].files[0];
dataType: 'jsonp',
jsonpCallback: 'process_return_${hlskey}', var reader = new FileReader();
beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); }, reader.onload = function(event) {
timeout : 7000, var contents = event.target.result;
success: function(result) { el.data('editor').setValue(contents);
console.log(result); // trigger compile & save
}, el.find('.hls-compile').trigger('click');
error: function() { };
alert('Error: cannot connect to latex2edx server'); reader.readAsText(file);
console.log('error!'); });
}
// refresh button
hlsmodal.find('.hls-refresh').click(function() {
refresh_hls(hlsmodal);
});
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(); 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
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}'));
} }
} });
}
function set_raw_edit_box(data,key){
// get the codemirror editor for the raw-edit-box function process_return_${hlskey}(datadict) {
// it's a CodeMirror-wrap class element // datadict is json of array with "xml" and "message"
$('#hls-modal-'+key).closest('.component').find('.CodeMirror-wrap')[0].CodeMirror.setValue(data); // if "xml" value is '' then the conversion failed
} var xml = datadict.xml;
if (xml.length == 0) {
// save button alert('Conversion failed! error:' + datadict.message);
} else {
$('#hls-modal-${hlskey}').find('.hls-save').click(function(){save_hls($('#hls-modal-${hlskey}'))}); set_raw_edit_box(xml, '${hlskey}');
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();
}
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
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> </script>
...@@ -47,6 +47,10 @@ urlpatterns = ('', ...@@ -47,6 +47,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-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-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'), 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'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'),
......
...@@ -65,7 +65,23 @@ def is_commentable_cohorted(course_id, commentable_id): ...@@ -65,7 +65,23 @@ def is_commentable_cohorted(course_id, commentable_id):
ans)) ans))
return ans 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): def get_cohort(user, course_id):
""" """
Given a django User and a course_id, return the user's cohort in that Given a django User and a course_id, return the user's cohort in that
......
...@@ -385,7 +385,7 @@ def login_user(request, error=""): ...@@ -385,7 +385,7 @@ def login_user(request, error=""):
try: try:
login(request, user) login(request, user)
if request.POST.get('remember') == 'true': if request.POST.get('remember') == 'true':
request.session.set_expiry(None) # or change to 604800 for 7 days request.session.set_expiry(604800)
log.debug("Setting user session to never expire") log.debug("Setting user session to never expire")
else: else:
request.session.set_expiry(0) request.session.set_expiry(0)
......
from student.models import User, UserProfile, Registration from student.models import User, UserProfile, Registration
from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import Factory from factory import Factory
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -8,6 +9,12 @@ from uuid import uuid4 ...@@ -8,6 +9,12 @@ from uuid import uuid4
from xmodule.timeparse import stringify_time from xmodule.timeparse import stringify_time
class GroupFactory(Factory):
FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course'
class UserProfileFactory(Factory): class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile FACTORY_FOR = UserProfile
......
from lettuce import world, step from lettuce import world, step
from factories import * from factories import *
from django.core.management import call_command
from lettuce.django import django_url from lettuce.django import django_url
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from student.models import CourseEnrollment from student.models import CourseEnrollment
from urllib import quote_plus from urllib import quote_plus
...@@ -21,6 +19,11 @@ def wait(step, seconds): ...@@ -21,6 +19,11 @@ def wait(step, seconds):
time.sleep(float(seconds)) time.sleep(float(seconds))
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step('I (?:visit|access|open) the homepage$') @step('I (?:visit|access|open) the homepage$')
def i_visit_the_homepage(step): def i_visit_the_homepage(step):
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
...@@ -105,6 +108,11 @@ def i_am_an_edx_user(step): ...@@ -105,6 +108,11 @@ def i_am_an_edx_user(step):
#### helper functions #### helper functions
@world.absorb
def scroll_to_bottom():
# Maximize the browser
world.browser.execute_script("window.scrollTo(0, screen.height);")
@world.absorb @world.absorb
def create_user(uname): def create_user(uname):
......
<problem>
<choiceresponse>
<checkboxgroup>
<choice correct="false">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</checkboxgroup>
</choiceresponse>
<choiceresponse>
<checkboxgroup>
<choice correct="false">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</checkboxgroup>
</choiceresponse>
<choiceresponse>
<checkboxgroup>
<choice correct="false">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</checkboxgroup>
</choiceresponse>
</problem>
<problem>
<choiceresponse>
<radiogroup>
<choice correct="false">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</radiogroup>
</choiceresponse>
<choiceresponse>
<radiogroup>
<choice correct="false">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="true">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</radiogroup>
</choiceresponse>
</problem>
<problem>
<text>
<h2>Code response</h2>
<p>
</p>
<text>
Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<codeparam>
<initial_display>def square(x):</initial_display>
<answer_display>answer</answer_display>
<grader_payload>grader stuff</grader_payload>
</codeparam>
</coderesponse>
</text>
<text>
Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<codeparam>
<initial_display>def square(x):</initial_display>
<answer_display>answer</answer_display>
<grader_payload>grader stuff</grader_payload>
</codeparam>
</coderesponse>
</text>
</text>
</problem>
<problem>
<text>
<h2>Code response</h2>
<p>
</p>
<text>
Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[
initial_display = """
def square(n):
"""
answer = """
def square(n):
return n**2
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testSquare(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: square(%d)'%n
return str(square(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testSquare(0))
elif test == 2: f.write(testSquare(1))
else: f.write(testSquare())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse>
</text>
<text>
Write a program to compute the cube of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[
initial_display = """
def cube(n):
"""
answer = """
def cube(n):
return n**3
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testCube(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: cube(%d)'%n
return str(cube(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testCube(0))
elif test == 2: f.write(testCube(1))
else: f.write(testCube())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse>
</text>
</text>
</problem>
This file is used to test converting file handles to filenames in the capa utility module
<problem>
<script type="loncapa/python">
# from loncapa import *
x1 = 4 # lc_random(2,4,1)
y1 = 5 # lc_random(3,7,1)
x2 = 10 # lc_random(x1+1,9,1)
y2 = 20 # lc_random(y1+1,15,1)
m = (y2-y1)/(x2-x1)
b = y1 - m*x1
answer = "%s*x+%s" % (m,b)
answer = answer.replace('+-','-')
inverted_m = (x2-x1)/(y2-y1)
inverted_b = b
wrongans = "%s*x+%s" % (inverted_m,inverted_b)
wrongans = wrongans.replace('+-','-')
</script>
<text>
<p>Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.</p>
<p>
What is the equation of the line which passess through ($x1,$y1) and
($x2,$y2)?</p>
<p>The correct answer is <tt>$answer</tt>. A common error is to invert the equation for the slope. Enter <tt>
$wrongans</tt> to see a hint.</p>
</text>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text>
<hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint>
<hintpart on="inversegrad">
<text>You have inverted the slope in the question.</text>
</hintpart>
</hintgroup>
</formularesponse>
</problem>
<problem>
<text><p>
Two skiers are on frictionless black diamond ski slopes.
Hello</p></text>
<imageresponse max="1" loncapaid="11">
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)"/>
<text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/>
<text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
<imageresponse max="1" loncapaid="12">
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98)" regions='[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]'/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[[10,10], [20,10], [20, 30]]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [15, 15]]"/>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" regions="[[10,10], [30,30], [10, 30], [30, 10]]"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text>
</hintgroup>
</imageresponse>
</problem>
<problem>
<javascriptresponse>
<generator src="test_problem_generator.js"/>
<grader src="test_problem_grader.js"/>
<display class="TestProblemDisplay" src="test_problem_display.js"/>
<responseparam name="value" value="4"/>
<javascriptinput>
</javascriptinput>
</javascriptresponse>
</problem>
// Generated by CoffeeScript 1.3.3
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
MinimaxProblemDisplay = (function(_super) {
__extends(MinimaxProblemDisplay, _super);
function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
}
MinimaxProblemDisplay.prototype.render = function() {};
MinimaxProblemDisplay.prototype.createSubmission = function() {
var id, value, _ref, _results;
this.newSubmission = {};
if (this.submission != null) {
_ref = this.submission;
_results = [];
for (id in _ref) {
value = _ref[id];
_results.push(this.newSubmission[id] = value);
}
return _results;
}
};
MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
return this.newSubmission;
};
return MinimaxProblemDisplay;
})(XProblemDisplay);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.TestProblemDisplay = TestProblemDisplay;
}).call(this);
;
// Generated by CoffeeScript 1.3.3
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
MinimaxProblemDisplay = (function(_super) {
__extends(MinimaxProblemDisplay, _super);
function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
}
MinimaxProblemDisplay.prototype.render = function() {};
MinimaxProblemDisplay.prototype.createSubmission = function() {
var id, value, _ref, _results;
this.newSubmission = {};
if (this.submission != null) {
_ref = this.submission;
_results = [];
for (id in _ref) {
value = _ref[id];
_results.push(this.newSubmission[id] = value);
}
return _results;
}
};
MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
return this.newSubmission;
};
return MinimaxProblemDisplay;
})(XProblemDisplay);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.TestProblemDisplay = TestProblemDisplay;
}).call(this);
;
<problem>
<multiplechoiceresponse>
<choicegroup>
<choice correct="false" >
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false" >
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true" >
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
<problem>
<multiplechoiceresponse>
<choicegroup>
<choice correct="false" name="foil1">
<startouttext />This is foil One.<endouttext />
</choice>
<choice correct="false" name="foil2">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice correct="true" name="foil3">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice correct="false" name="foil4">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice correct="false" name="foil5">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
<problem>
<text>
<p>
Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture? <br/>
Assume that for both bicycles:<br/>
1.) The tires have equal air pressure.<br/>
2.) The bicycles never leave the contact with the bump.<br/>
3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.<br/>
</p>
</text>
<optionresponse texlayout="horizontal" max="10" randomize="yes">
<ul>
<li>
<text>
<p>The bicycles with larger wheels have more time to go over the bump. This decreases the magnitude of the force needed to lift the bicycle.</p>
</text>
<optioninput name="Foil1" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.</p>
</text>
<optioninput name="Foil2" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.</p>
</text>
<optioninput name="Foil3" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.</p>
</text>
<optioninput name="Foil4" location="random" options="('True','False')" correct="True">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.</p>
</text>
<optioninput name="Foil5" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
<li>
<text>
<p>The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.</p>
</text>
<optioninput name="Foil6" location="random" options="('True','False')" correct="False">
</optioninput>
</li>
</ul>
<hintgroup showoncorrect="no">
<text>
<br/>
<br/>
</text>
</hintgroup>
</optionresponse>
</problem>
<problem >
<text><h2>Example: String Response Problem</h2>
<br/>
</text>
<text>Which US state has Lansing as its capital?</text>
<stringresponse answer="Michigan" type="ci">
<textline size="20" />
<hintgroup>
<stringhint answer="wisconsin" type="cs" name="wisc">
</stringhint>
<stringhint answer="minnesota" type="cs" name="minn">
</stringhint>
<hintpart on="wisc">
<text>The state capital of Wisconsin is Madison.</text>
</hintpart>
<hintpart on="minn">
<text>The state capital of Minnesota is St. Paul.</text>
</hintpart>
<hintpart on="default">
<text>The state you are looking for is also known as the 'Great Lakes State'</text>
</hintpart>
</hintgroup>
</stringresponse>
</problem>
<problem>
<text>
<h2>Example: Symbolic Math Response Problem</h2>
<p>
A symbolic math response problem presents one or more symbolic math
input fields for input. Correctness of input is evaluated based on
the symbolic properties of the expression entered. The student enters
text, but sees a proper symbolic rendition of the entered formula, in
real time, next to the input box.
</p>
<p>This is a correct answer which may be entered below: </p>
<p><tt>cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]</tt></p>
<script>
from symmath import *
</script>
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 &amp; 1 \\ 1 &amp; 0 \end{matrix} \right] \right) [/mathjax]
and give the resulting \(2 \times 2\) matrix. <br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. <br/>
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),I*sin(theta)],[I*sin(theta),cos(theta)]]" options="matrix,imaginaryi" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
<textline size="80" math="1" response_id="2" answer_id="1" id="filenamedogi0VpEBOWedxsymmathresponse_2_1"/>
</symbolicresponse>
<br/>
</text>
</text>
</problem>
<problem>
<truefalseresponse max="10" randomize="yes">
<choicegroup>
<choice location="random" correct="true" name="foil1">
<startouttext />This is foil One.<endouttext />
</choice>
<choice location="random" correct="true" name="foil2">
<startouttext />This is foil Two.<endouttext />
</choice>
<choice location="random" correct="false" name="foil3">
<startouttext />This is foil Three.<endouttext />
</choice>
<choice location="random" correct="false" name="foil4">
<startouttext />This is foil Four.<endouttext />
</choice>
<choice location="random" correct="false" name="foil5">
<startouttext />This is foil Five.<endouttext />
</choice>
</choicegroup>
</truefalseresponse>
</problem>
...@@ -111,7 +111,7 @@ class DragAndDrop(object): ...@@ -111,7 +111,7 @@ class DragAndDrop(object):
Returns: bool. Returns: bool.
''' '''
for draggable in self.excess_draggables: for draggable in self.excess_draggables:
if not self.excess_draggables[draggable]: if self.excess_draggables[draggable]:
return False # user answer has more draggables than correct answer return False # user answer has more draggables than correct answer
# Number of draggables in user_groups may be differ that in # Number of draggables in user_groups may be differ that in
...@@ -304,8 +304,13 @@ class DragAndDrop(object): ...@@ -304,8 +304,13 @@ class DragAndDrop(object):
user_answer = json.loads(user_answer) user_answer = json.loads(user_answer)
# check if we have draggables that are not in correct answer: # This dictionary will hold a key for each draggable the user placed on
self.excess_draggables = {} # the image. The value is True if that draggable is not mentioned in any
# correct_answer entries. If the draggable is mentioned in at least one
# correct_answer entry, the value is False.
# default to consider every user answer excess until proven otherwise.
self.excess_draggables = dict((users_draggable.keys()[0],True)
for users_draggable in user_answer['draggables'])
# create identical data structures from user answer and correct answer # create identical data structures from user answer and correct answer
for i in xrange(0, len(correct_answer)): for i in xrange(0, len(correct_answer)):
...@@ -322,11 +327,8 @@ class DragAndDrop(object): ...@@ -322,11 +327,8 @@ class DragAndDrop(object):
self.user_groups[groupname].append(draggable_name) self.user_groups[groupname].append(draggable_name)
self.user_positions[groupname]['user'].append( self.user_positions[groupname]['user'].append(
draggable_dict[draggable_name]) draggable_dict[draggable_name])
self.excess_draggables[draggable_name] = True # proved that this is not excess
else: self.excess_draggables[draggable_name] = False
self.excess_draggables[draggable_name] = \
self.excess_draggables.get(draggable_name, False)
def grade(user_input, correct_answer): def grade(user_input, correct_answer):
""" Creates DragAndDrop instance from user_input and correct_answer and """ Creates DragAndDrop instance from user_input and correct_answer and
......
...@@ -46,6 +46,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase): ...@@ -46,6 +46,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
correct_answer = {'1': 't1', 'name_with_icon': 't2'} correct_answer = {'1': 't1', 'name_with_icon': 't2'}
self.assertTrue(draganddrop.grade(user_input, correct_answer)) self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_expect_no_actions_wrong(self):
user_input = '{"draggables": [{"1": "t1"}, \
{"name_with_icon": "t2"}]}'
correct_answer = []
self.assertFalse(draganddrop.grade(user_input, correct_answer))
def test_expect_no_actions_right(self):
user_input = '{"draggables": []}'
correct_answer = []
self.assertTrue(draganddrop.grade(user_input, correct_answer))
def test_targets_false(self): def test_targets_false(self):
user_input = '{"draggables": [{"1": "t1"}, \ user_input = '{"draggables": [{"1": "t1"}, \
{"name_with_icon": "t2"}]}' {"name_with_icon": "t2"}]}'
......
...@@ -4,5 +4,5 @@ setup( ...@@ -4,5 +4,5 @@ setup(
name="capa", name="capa",
version="0.1", version="0.1",
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=["tests"]),
install_requires=['distribute', 'pyparsing'], install_requires=['distribute==0.6.34', 'pyparsing==1.5.6'],
) )
...@@ -333,6 +333,12 @@ class CapaModule(XModule): ...@@ -333,6 +333,12 @@ class CapaModule(XModule):
reset_button = False reset_button = False
save_button = False save_button = False
# If attempts=0 then show just check and reset buttons; this is for survey questions using capa
if self.max_attempts==0:
check_button = False
reset_button = True
save_button = True
# User submitted a problem, and hasn't reset. We don't want # User submitted a problem, and hasn't reset. We don't want
# more submissions. # more submissions.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
...@@ -630,11 +636,11 @@ class CapaModule(XModule): ...@@ -630,11 +636,11 @@ class CapaModule(XModule):
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed() and not self.max_attempts==0:
event_info['failure'] = 'closed' event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
'error': "Problem is closed"} 'msg': "Problem is closed"}
# Problem submitted. Student should reset before saving # Problem submitted. Student should reset before saving
# again. # again.
...@@ -642,13 +648,16 @@ class CapaModule(XModule): ...@@ -642,13 +648,16 @@ class CapaModule(XModule):
event_info['failure'] = 'done' event_info['failure'] = 'done'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
'error': "Problem needs to be reset prior to save."} 'msg': "Problem needs to be reset prior to save"}
self.lcp.student_answers = answers self.lcp.student_answers = answers
# TODO: should this be save_problem_fail? Looks like success to me... self.system.track_function('save_problem_success', event_info)
self.system.track_function('save_problem_fail', event_info) msg = "Your answers have been saved"
return {'success': True} if not self.max_attempts==0:
msg += " but not graded. Hit 'Check' to grade them."
return {'success': True,
'msg': msg}
def reset_problem(self, get): def reset_problem(self, get):
''' Changes problem state to unfinished -- removes student answers, ''' Changes problem state to unfinished -- removes student answers,
......
...@@ -4,9 +4,8 @@ from lxml import etree ...@@ -4,9 +4,8 @@ from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
from .editing_module import EditingDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from .xml_module import XmlDescriptor
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -134,7 +133,7 @@ class CombinedOpenEndedModule(XModule):
} }
self.child_descriptor = descriptors[version_index](self.system) self.child_descriptor = descriptors[version_index](self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['xml_string']), self.system) self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(definition['data']), self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor, self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data) instance_state = json.dumps(instance_state), metadata = self.metadata, static_data= static_data)
...@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule): ...@@ -165,11 +164,11 @@ class CombinedOpenEndedModule(XModule):
return self.child_module.display_name return self.child_module.display_name
class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): class CombinedOpenEndedDescriptor(RawDescriptor):
""" """
Module for adding combined open ended questions Module for adding combined open ended questions
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/raw-edit.html"
module_class = CombinedOpenEndedModule module_class = CombinedOpenEndedModule
filename_extension = "xml" filename_extension = "xml"
...@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -177,35 +176,3 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
has_score = True has_score = True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the individual tasks, the rubric, and the prompt, and parse
Returns:
{
'rubric': 'some-html',
'prompt': 'some-html',
'task_xml': dictionary of xml strings,
}
"""
return {'xml_string' : etree.tostring(xml_object), 'metadata' : xml_object.attrib}
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt
...@@ -40,7 +40,7 @@ section.problem { ...@@ -40,7 +40,7 @@ section.problem {
@include clearfix; @include clearfix;
label.choicegroup_correct{ label.choicegroup_correct{
text:after{ &:after{
content: url('../images/correct-icon.png'); content: url('../images/correct-icon.png');
} }
} }
......
...@@ -119,13 +119,13 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -119,13 +119,13 @@ describe 'MarkdownEditingDescriptor', ->
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p> <p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
<p>Enter the numerical value of Pi:</p> <p>Enter the numerical value of Pi:</p>
<numericalresponse answer="3.14159 "> <numericalresponse answer="3.14159">
<responseparam type="tolerance" default=".02" /> <responseparam type="tolerance" default=".02" />
<textline /> <textline />
</numericalresponse> </numericalresponse>
<p>Enter the approximate value of 502*9:</p> <p>Enter the approximate value of 502*9:</p>
<numericalresponse answer="4518 "> <numericalresponse answer="4518">
<responseparam type="tolerance" default="15%" /> <responseparam type="tolerance" default="15%" />
<textline /> <textline />
</numericalresponse> </numericalresponse>
...@@ -148,6 +148,20 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -148,6 +148,20 @@ describe 'MarkdownEditingDescriptor', ->
</div> </div>
</solution> </solution>
</problem>""") </problem>""")
it 'will convert 0 as a numerical response (instead of string response)', ->
data = MarkdownEditingDescriptor.markdownToXml("""
Enter 0 with a tolerance:
= 0 +- .02
""")
expect(data).toEqual("""<problem>
<p>Enter 0 with a tolerance:</p>
<numericalresponse answer="0">
<responseparam type="tolerance" default=".02" />
<textline />
</numericalresponse>
</problem>""")
it 'converts multiple choice to xml', -> it 'converts multiple choice to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets. data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
......
...@@ -262,9 +262,8 @@ class @Problem ...@@ -262,9 +262,8 @@ class @Problem
save: => save: =>
Logger.log 'problem_save', @answers Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) => $.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
if response.success saveMessage = response.msg
saveMessage = "Your answers have been saved but not graded. Hit 'Check' to grade them." @gentle_alert saveMessage
@gentle_alert saveMessage
@updateProgress response @updateProgress response
refreshMath: (event, element) => refreshMath: (event, element) =>
......
...@@ -4,7 +4,47 @@ class @Rubric ...@@ -4,7 +4,47 @@ class @Rubric
@initialize: (location) -> @initialize: (location) ->
$('.rubric').data("location", location) $('.rubric').data("location", location)
$('input[class="score-selection"]').change @tracking_callback $('input[class="score-selection"]').change @tracking_callback
# set up the hotkeys
$(window).unbind('keydown', @keypress_callback)
$(window).keydown @keypress_callback
# display the 'current' carat
@categories = $('.rubric-category')
@category = $(@categories.first())
@category.prepend('> ')
@category_index = 0
@keypress_callback: (event) =>
# don't try to do this when user is typing in a text input
if $(event.target).is('input, textarea')
return
# for when we select via top row
if event.which >= 48 and event.which <= 57
selected = event.which - 48
# for when we select via numpad
else if event.which >= 96 and event.which <= 105
selected = event.which - 96
# we don't want to do anything since we haven't pressed a number
else
return
# if we actually have a current category (not past the end)
if(@category_index <= @categories.length)
# find the valid selections for this category
inputs = $("input[name='score-selection-#{@category_index}']")
max_score = inputs.length - 1
if selected > max_score or selected < 0
return
inputs.filter("input[value=#{selected}]").click()
# move to the next category
old_category_text = @category.html().substring(5)
@category.html(old_category_text)
@category_index++
@category = $(@categories[@category_index])
@category.prepend('> ')
@tracking_callback: (event) -> @tracking_callback: (event) ->
target_selection = $(event.target).val() target_selection = $(event.target).val()
# chop off the beginning of the name so that we can get the number of the category # chop off the beginning of the name so that we can get the number of the category
...@@ -49,6 +89,7 @@ class @CombinedOpenEnded ...@@ -49,6 +89,7 @@ class @CombinedOpenEnded
constructor: (element) -> constructor: (element) ->
@element=element @element=element
@reinitialize(element) @reinitialize(element)
$(window).keydown @keydown_handler
reinitialize: (element) -> reinitialize: (element) ->
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule') @wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
...@@ -306,6 +347,7 @@ class @CombinedOpenEnded ...@@ -306,6 +347,7 @@ class @CombinedOpenEnded
if response.success if response.success
@rubric_wrapper.html(response.rubric_html) @rubric_wrapper.html(response.rubric_html)
@rubric_wrapper.show() @rubric_wrapper.show()
Rubric.initialize(@location)
@answer_area.html(response.student_response) @answer_area.html(response.student_response)
@child_state = 'assessing' @child_state = 'assessing'
@find_assessment_elements() @find_assessment_elements()
...@@ -318,6 +360,11 @@ class @CombinedOpenEnded ...@@ -318,6 +360,11 @@ class @CombinedOpenEnded
else else
@errors_area.html(@out_of_sync_message) @errors_area.html(@out_of_sync_message)
keydown_handler: (e) =>
# only do anything when the key pressed is the 'enter' key
if e.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
@save_assessment(e)
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
if @child_state == 'assessing' && Rubric.check_complete() if @child_state == 'assessing' && Rubric.check_complete()
......
...@@ -210,6 +210,9 @@ class @PeerGradingProblem ...@@ -210,6 +210,9 @@ class @PeerGradingProblem
@calibration_interstitial_page_button = $('.calibration-interstitial-page-button') @calibration_interstitial_page_button = $('.calibration-interstitial-page-button')
@flag_student_checkbox = $('.flag-checkbox') @flag_student_checkbox = $('.flag-checkbox')
@answer_unknown_checkbox = $('.answer-unknown-checkbox') @answer_unknown_checkbox = $('.answer-unknown-checkbox')
$(window).keydown @keydown_handler
@collapse_question() @collapse_question()
Collapsible.setCollapsibles(@content_panel) Collapsible.setCollapsibles(@content_panel)
...@@ -251,9 +254,6 @@ class @PeerGradingProblem ...@@ -251,9 +254,6 @@ class @PeerGradingProblem
fetch_submission_essay: () => fetch_submission_essay: () =>
@backend.post('get_next_submission', {location: @location}, @render_submission) @backend.post('get_next_submission', {location: @location}, @render_submission)
gentle_alert: (msg) =>
@grading_message.fadeIn()
@grading_message.html("<p>" + msg + "</p>")
construct_data: () -> construct_data: () ->
data = data =
...@@ -337,6 +337,14 @@ class @PeerGradingProblem ...@@ -337,6 +337,14 @@ class @PeerGradingProblem
@show_submit_button() @show_submit_button()
@grade = Rubric.get_total_score() @grade = Rubric.get_total_score()
keydown_handler: (event) =>
if event.which == 13 && @submit_button.is(':visible')
if @calibration
@submit_calibration_essay()
else
@submit_grade()
########## ##########
...@@ -473,6 +481,10 @@ class @PeerGradingProblem ...@@ -473,6 +481,10 @@ class @PeerGradingProblem
# And now hook up an event handler again # And now hook up an event handler again
$("input[class='score-selection']").change @graded_callback $("input[class='score-selection']").change @graded_callback
gentle_alert: (msg) =>
@grading_message.fadeIn()
@grading_message.html("<p>" + msg + "</p>")
collapse_question: () => collapse_question: () =>
@prompt_container.slideToggle() @prompt_container.slideToggle()
@prompt_container.toggleClass('open') @prompt_container.toggleClass('open')
......
...@@ -231,13 +231,14 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -231,13 +231,14 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
// replace string and numerical // replace string and numerical
xml = xml.replace(/^\=\s*(.*?$)/gm, function(match, p) { xml = xml.replace(/^\=\s*(.*?$)/gm, function(match, p) {
var string; var string;
var params = /(.*?)\+\-\s*(.*?$)/.exec(p); var floatValue = parseFloat(p);
if(parseFloat(p)) { if(!isNaN(floatValue)) {
var params = /(.*?)\+\-\s*(.*?$)/.exec(p);
if(params) { if(params) {
string = '<numericalresponse answer="' + params[1] + '">\n'; string = '<numericalresponse answer="' + floatValue + '">\n';
string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n'; string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n';
} else { } else {
string = '<numericalresponse answer="' + p + '">\n'; string = '<numericalresponse answer="' + floatValue + '">\n';
} }
string += ' <textline />\n'; string += ' <textline />\n';
string += '</numericalresponse>\n\n'; string += '</numericalresponse>\n\n';
......
...@@ -157,10 +157,15 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -157,10 +157,15 @@ class MongoModuleStore(ModuleStoreBase):
''' '''
# get all collections in the course, this query should not return any leaf nodes # get all collections in the course, this query should not return any leaf nodes
query = { '_id.org' : location.org, query = {
'_id.course' : location.course, '_id.org': location.org,
'_id.revision' : None, '_id.course': location.course,
'definition.children':{'$ne': []} '$or': [
{"_id.category":"course"},
{"_id.category":"chapter"},
{"_id.category":"sequential"},
{"_id.category":"vertical"}
]
} }
# we just want the Location, children, and metadata # we just want the Location, children, and metadata
record_filter = {'_id':1,'definition.children':1,'metadata':1} record_filter = {'_id':1,'definition.children':1,'metadata':1}
...@@ -279,6 +284,13 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -279,6 +284,13 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root) resource_fs = OSFS(root)
metadata_inheritance_tree = None
# if we are loading a course object, there is no parent to inherit the metadata from
# so don't bother getting it
if item['location']['category'] != 'course':
metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']), 300)
# TODO (cdodge): When the 'split module store' work has been completed, we should remove # TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter # the 'metadata_inheritance_tree' parameter
system = CachingDescriptorSystem( system = CachingDescriptorSystem(
...@@ -288,7 +300,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -288,7 +300,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs, resource_fs,
self.error_tracker, self.error_tracker,
self.render_template, self.render_template,
metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']), 60) metadata_inheritance_tree = metadata_inheritance_tree
) )
return system.load_item(item['location']) return system.load_item(item['location'])
......
...@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children ...@@ -22,7 +22,7 @@ from xmodule.stringify import stringify_children
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from capa.util import * from capa.util import *
from peer_grading_service import PeerGradingService from peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service import controller_query_service
from datetime import datetime from datetime import datetime
...@@ -106,8 +106,14 @@ class OpenEndedChild(object): ...@@ -106,8 +106,14 @@ class OpenEndedChild(object):
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = static_data['max_score'] self._max_score = static_data['max_score']
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system) if system.open_ended_grading_interface:
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system) self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,system)
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
self.system = system self.system = system
...@@ -461,11 +467,14 @@ class OpenEndedChild(object): ...@@ -461,11 +467,14 @@ class OpenEndedChild(object):
return success, allowed_to_submit, error_message return success, allowed_to_submit, error_message
def get_eta(self): def get_eta(self):
response = self.controller_qs.check_for_eta(self.location_string) if self.controller_qs:
try: response = self.controller_qs.check_for_eta(self.location_string)
response = json.loads(response) try:
except: response = json.loads(response)
pass except:
pass
else:
return ""
success = response['success'] success = response['success']
if isinstance(success, basestring): if isinstance(success, basestring):
......
...@@ -14,7 +14,7 @@ from xmodule.modulestore import Location ...@@ -14,7 +14,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from timeinfo import TimeInfo from timeinfo import TimeInfo
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -53,13 +53,28 @@ class PeerGradingModule(XModule): ...@@ -53,13 +53,28 @@ class PeerGradingModule(XModule):
#We need to set the location here so the child modules can use it #We need to set the location here so the child modules can use it
system.set('location', location) system.set('location', location)
self.system = system self.system = system
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system) if(self.system.open_ended_grading_interface):
self.peer_gs = PeerGradingService(self.system.open_ended_grading_interface, self.system)
else:
self.peer_gs = MockPeerGradingService()
self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION) self.use_for_single_location = self.metadata.get('use_for_single_location', USE_FOR_SINGLE_LOCATION)
if isinstance(self.use_for_single_location, basestring): if isinstance(self.use_for_single_location, basestring):
self.use_for_single_location = (self.use_for_single_location in TRUE_DICT) self.use_for_single_location = (self.use_for_single_location in TRUE_DICT)
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
if self.use_for_single_location == True:
try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except:
log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location))
raise
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
if due_date:
self.metadata['due'] = due_date
self.is_graded = self.metadata.get('is_graded', IS_GRADED) self.is_graded = self.metadata.get('is_graded', IS_GRADED)
if isinstance(self.is_graded, basestring): if isinstance(self.is_graded, basestring):
self.is_graded = (self.is_graded in TRUE_DICT) self.is_graded = (self.is_graded in TRUE_DICT)
...@@ -75,17 +90,6 @@ class PeerGradingModule(XModule): ...@@ -75,17 +90,6 @@ class PeerGradingModule(XModule):
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
self.link_to_location = self.metadata.get('link_to_location', USE_FOR_SINGLE_LOCATION)
if self.use_for_single_location == True:
try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except:
log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location))
raise
due_date = self.linked_problem.metadata.get('peer_grading_due', None)
if due_date:
self.metadata['due'] = due_date
self.ajax_url = self.system.ajax_url self.ajax_url = self.system.ajax_url
if not self.ajax_url.endswith("/"): if not self.ajax_url.endswith("/"):
...@@ -562,7 +566,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -562,7 +566,7 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
""" """
Module for adding combined open ended questions Module for adding combined open ended questions
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/raw-edit.html"
module_class = PeerGradingModule module_class = PeerGradingModule
filename_extension = "xml" filename_extension = "xml"
...@@ -606,13 +610,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -606,13 +610,4 @@ class PeerGradingDescriptor(XmlDescriptor, EditingDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.''' '''Return an xml element representing this definition.'''
elt = etree.Element('peergrading') elt = etree.Element('peergrading')
def add_child(k):
child_str = '<{tag}>{body}</{tag}>'.format(tag=k, body=self.definition[k])
child_node = etree.fromstring(child_str)
elt.append(child_node)
for child in ['task']:
add_child(child)
return elt return elt
--- ---
metadata: metadata:
display_name: Problem Written in LaTeX display_name: Problem Written in LaTeX
source_processor_url: https://qisx.mit.edu:5443/latex2edx source_processor_url: https://studio-input-filter.mitx.mit.edu/latex2edx
source_code: | source_code: |
% Nearly any kind of edX problem can be authored using Latex as % Nearly any kind of edX problem can be authored using Latex as
% the source language. Write latex as usual, including equations. The % the source language. Write latex as usual, including equations. The
......
...@@ -39,6 +39,8 @@ if Backbone? ...@@ -39,6 +39,8 @@ if Backbone?
url = DiscussionUtil.urlFor 'threads' url = DiscussionUtil.urlFor 'threads'
when 'followed' when 'followed'
url = DiscussionUtil.urlFor 'followed_threads', options.user_id url = DiscussionUtil.urlFor 'followed_threads', options.user_id
if options['group_id']
data['group_id'] = options['group_id']
data['sort_key'] = sort_options.sort_key || 'date' data['sort_key'] = sort_options.sort_key || 'date'
data['sort_order'] = sort_options.sort_order || 'desc' data['sort_order'] = sort_options.sort_order || 'desc'
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
......
...@@ -70,10 +70,21 @@ if Backbone? ...@@ -70,10 +70,21 @@ if Backbone?
DiscussionUtil.loadRoles(response.roles) DiscussionUtil.loadRoles(response.roles)
allow_anonymous = response.allow_anonymous allow_anonymous = response.allow_anonymous
allow_anonymous_to_peers = response.allow_anonymous_to_peers allow_anonymous_to_peers = response.allow_anonymous_to_peers
cohorts = response.cohorts
# $elem.html("Hide Discussion") # $elem.html("Hide Discussion")
@discussion = new Discussion() @discussion = new Discussion()
@discussion.reset(response.discussion_data, {silent: false}) @discussion.reset(response.discussion_data, {silent: false})
$discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous})
#use same discussion template but different thread templated
#determined in the coffeescript based on whether or not there's a
#group id
if response.is_cohorted
source = "script#_inline_discussion_cohorted"
else
source = "script#_inline_discussion"
$discussion = $(Mustache.render $(source).html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous, 'cohorts':cohorts})
if @$('section.discussion').length if @$('section.discussion').length
@$('section.discussion').replaceWith($discussion) @$('section.discussion').replaceWith($discussion)
else else
......
...@@ -9,6 +9,7 @@ if Backbone? ...@@ -9,6 +9,7 @@ if Backbone?
"click .browse-topic-drop-search-input": "ignoreClick" "click .browse-topic-drop-search-input": "ignoreClick"
"click .post-list .list-item a": "threadSelected" "click .post-list .list-item a": "threadSelected"
"click .post-list .more-pages a": "loadMorePages" "click .post-list .more-pages a": "loadMorePages"
"change .cohort-options": "chooseCohort"
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop 'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
initialize: -> initialize: ->
...@@ -128,10 +129,20 @@ if Backbone? ...@@ -128,10 +129,20 @@ if Backbone?
switch @mode switch @mode
when 'search' when 'search'
options.search_text = @current_search options.search_text = @current_search
if @group_id
options.group_id = @group_id
when 'followed' when 'followed'
options.user_id = window.user.id options.user_id = window.user.id
options.group_id = "all"
when 'commentables' when 'commentables'
options.commentable_ids = @discussionIds options.commentable_ids = @discussionIds
if @group_id
options.group_id = @group_id
when 'all'
if @group_id
options.group_id = @group_id
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy}) @collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy})
renderThread: (thread) => renderThread: (thread) =>
...@@ -263,13 +274,25 @@ if Backbone? ...@@ -263,13 +274,25 @@ if Backbone?
if discussionId == "#all" if discussionId == "#all"
@discussionIds = "" @discussionIds = ""
@$(".post-search-field").val("") @$(".post-search-field").val("")
@$('.cohort').show()
@retrieveAllThreads() @retrieveAllThreads()
else if discussionId == "#following" else if discussionId == "#following"
@retrieveFollowed(event) @retrieveFollowed(event)
@$('.cohort').hide()
else else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
@retrieveDiscussions(discussionIds)
if $(event.target).attr('cohorted') == "True"
@retrieveDiscussions(discussionIds, "function(){$('.cohort').show();}")
else
@retrieveDiscussions(discussionIds, "function(){$('.cohort').hide();}")
chooseCohort: (event) ->
@group_id = @$('.cohort-options :selected').val()
@collection.current_page = 0
@collection.reset()
@loadMorePages(event)
retrieveDiscussion: (discussion_id, callback=null) -> retrieveDiscussion: (discussion_id, callback=null) ->
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id) url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
......
...@@ -16,7 +16,10 @@ if Backbone? ...@@ -16,7 +16,10 @@ if Backbone?
@$delegateElement = @$local @$delegateElement = @$local
render: -> render: ->
@template = DiscussionUtil.getTemplate("_inline_thread") if @model.has('group_id')
@template = DiscussionUtil.getTemplate("_inline_thread_cohorted")
else
@template = DiscussionUtil.getTemplate("_inline_thread")
if not @model.has('abbreviatedBody') if not @model.has('abbreviatedBody')
@abbreviateBody() @abbreviateBody()
......
...@@ -25,6 +25,7 @@ if Backbone? ...@@ -25,6 +25,7 @@ if Backbone?
event.preventDefault() event.preventDefault()
title = @$(".new-post-title").val() title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val() body = @$(".new-post-body").find(".wmd-input").val()
group = @$(".new-post-group option:selected").attr("value")
# TODO tags: commenting out til we know what to do with them # TODO tags: commenting out til we know what to do with them
#tags = @$(".new-post-tags").val() #tags = @$(".new-post-tags").val()
...@@ -45,6 +46,7 @@ if Backbone? ...@@ -45,6 +46,7 @@ if Backbone?
data: data:
title: title title: title
body: body body: body
group_id: group
# TODO tags: commenting out til we know what to do with them # TODO tags: commenting out til we know what to do with them
#tags: tags #tags: tags
......
...@@ -14,8 +14,14 @@ if Backbone? ...@@ -14,8 +14,14 @@ if Backbone?
@setSelectedTopic() @setSelectedTopic()
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body" DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions() @$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
if @$($(".topic_menu li a")[0]).attr('cohorted') != "True"
$('.choose-cohort').hide();
events: events:
"submit .new-post-form": "createPost" "submit .new-post-form": "createPost"
"click .topic_dropdown_button": "toggleTopicDropdown" "click .topic_dropdown_button": "toggleTopicDropdown"
...@@ -65,6 +71,11 @@ if Backbone? ...@@ -65,6 +71,11 @@ if Backbone?
@topicText = @getFullTopicName($target) @topicText = @getFullTopicName($target)
@topicId = $target.data('discussion_id') @topicId = $target.data('discussion_id')
@setSelectedTopic() @setSelectedTopic()
if $target.attr('cohorted') == "True"
$('.choose-cohort').show();
else
$('.choose-cohort').hide();
setSelectedTopic: -> setSelectedTopic: ->
@dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>') @dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>')
...@@ -116,6 +127,7 @@ if Backbone? ...@@ -116,6 +127,7 @@ if Backbone?
title = @$(".new-post-title").val() title = @$(".new-post-title").val()
body = @$(".new-post-body").find(".wmd-input").val() body = @$(".new-post-body").find(".wmd-input").val()
tags = @$(".new-post-tags").val() tags = @$(".new-post-tags").val()
group = @$(".new-post-group option:selected").attr("value")
anonymous = false || @$("input.discussion-anonymous").is(":checked") anonymous = false || @$("input.discussion-anonymous").is(":checked")
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked") anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
...@@ -137,6 +149,7 @@ if Backbone? ...@@ -137,6 +149,7 @@ if Backbone?
anonymous: anonymous anonymous: anonymous
anonymous_to_peers: anonymous_to_peers anonymous_to_peers: anonymous_to_peers
auto_subscribe: follow auto_subscribe: follow
group_id: group
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors")) error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
success: (response, textStatus) => success: (response, textStatus) =>
# TODO: Move this out of the callback, this makes it feel sluggish # TODO: Move this out of the callback, this makes it feel sluggish
......
...@@ -85,13 +85,16 @@ select { ...@@ -85,13 +85,16 @@ select {
} }
#viewerContainer { #viewerContainer {
overflow: auto; /* overflow: auto; */
box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05); box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
/* position: absolute; /* position: absolute;
top: 32px; top: 32px;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; */ left: 0; */
/* switch to using these instead: */
position: relative;
overflow: hidden;
} }
.toolbar { .toolbar {
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
// to find the URL path in which the script lives. If the name // to find the URL path in which the script lives. If the name
// of the file is changed, GWT won't load correctly // of the file is changed, GWT won't load correctly
var jsmolcalc_src = '/sta' + 'tic/js/capa/jsmolcalc/jsmolcalc.nocache.js'; var jsmolcalc_src = '/sta' + 'tic/js/capa/jsmolcalc/jsmolcalc.nocache.js';
var jsme_src = '/sta' + 'tic/js/capa/jsme/jsme_export.nocache.js'; var jsme_src = '/sta' + 'tic/js/capa/jsme/jsme.nocache.js';
// Make sure we don't request the scripts twice // Make sure we don't request the scripts twice
...@@ -48,11 +48,11 @@ ...@@ -48,11 +48,11 @@
jsmolcalc.onInjectionDone('jsmolcalc'); jsmolcalc.onInjectionDone('jsmolcalc');
} }
if (typeof(jsme_export) != 'undefined' && jsme_export) if (typeof(jsme) != 'undefined' && jsme)
{ {
// dummy function called by jsme_export // dummy function called by jsme
window.jsmeOnLoad = function() {}; window.jsmeOnLoad = function() {};
jsme_export.onInjectionDone('jsme_export'); jsme.onInjectionDone('jsme');
} }
// jsmol is defined my jsmolcalc and JavaScriptApplet is defined by jsme // jsmol is defined my jsmolcalc and JavaScriptApplet is defined by jsme
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment