Commit d1e35b14 by Will Daly

Merge branch 'master' into feature/will/ci-lettuce-tests

parents baa7916a e122efc5
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
*.swp *.swp
*.orig *.orig
*.DS_Store *.DS_Store
*.mo
:2e_* :2e_*
:2e# :2e#
.AppleDouble .AppleDouble
...@@ -22,6 +23,8 @@ reports/ ...@@ -22,6 +23,8 @@ reports/
*.egg-info *.egg-info
Gemfile.lock Gemfile.lock
.env/ .env/
conf/locale/en/LC_MESSAGES/*.po
!messages.po
lms/static/sass/*.css lms/static/sass/*.css
cms/static/sass/*.css cms/static/sass/*.css
lms/lib/comment_client/python lms/lib/comment_client/python
......
[main]
host = https://www.transifex.com
[edx-studio.django-partial]
file_filter = conf/locale/<lang>/LC_MESSAGES/django-partial.po
source_file = conf/locale/en/LC_MESSAGES/django-partial.po
source_lang = en
type = PO
[edx-studio.djangojs]
file_filter = conf/locale/<lang>/LC_MESSAGES/djangojs.po
source_file = conf/locale/en/LC_MESSAGES/djangojs.po
source_lang = en
type = PO
[edx-studio.mako]
file_filter = conf/locale/<lang>/LC_MESSAGES/mako.po
source_file = conf/locale/en/LC_MESSAGES/mako.po
source_lang = en
type = PO
[edx-studio.messages]
file_filter = conf/locale/<lang>/LC_MESSAGES/messages.po
source_file = conf/locale/en/LC_MESSAGES/messages.po
source_lang = en
type = PO
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
...@@ -6,4 +7,4 @@ class Command(BaseCommand): ...@@ -6,4 +7,4 @@ class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB' help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options): def handle(self, *args, **options):
update_templates() update_templates(modulestore('direct'))
...@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -220,6 +220,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
num_drafts = self._get_draft_counts(course) num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1) self.assertEqual(num_drafts, 1)
def test_import_textbook_as_content_element(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -293,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -293,7 +301,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure the parent no longer points to the child object which was deleted # make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children) self.assertFalse(sequential.location.url() in chapter.children)
def test_about_overrides(self): def test_about_overrides(self):
''' '''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
...@@ -490,6 +497,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -490,6 +497,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(test_private_vertical, 'is_draft', False)) self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
# make sure the textbook survived the export/import
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
shutil.rmtree(root_dir) shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self): def test_course_handouts_rewrites(self):
...@@ -937,7 +949,7 @@ class TemplateTestCase(ModuleStoreTestCase): ...@@ -937,7 +949,7 @@ class TemplateTestCase(ModuleStoreTestCase):
self.assertIsNotNone(verify_create) self.assertIsNotNone(verify_create)
# now run cleanup # now run cleanup
update_templates() update_templates(modulestore('direct'))
# now try to find dangling template, it should not be in DB any longer # now try to find dangling template, it should not be in DB any longer
asserted = False asserted = False
......
...@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase): ...@@ -10,9 +10,9 @@ class CourseUpdateTest(CourseTestCase):
'''Go through each interface and ensure it works.''' '''Go through each interface and ensure it works.'''
# first get the update to force the creation # first get the update to force the creation
url = reverse('course_info', url = reverse('course_info',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'name': self.course_location.name}) 'name': self.course_location.name})
self.client.get(url) self.client.get(url)
init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">' init_content = '<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
...@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase): ...@@ -20,9 +20,9 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 8, 2013'} 'date': 'January 8, 2013'}
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
...@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase): ...@@ -31,25 +31,25 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(payload['content'], content) self.assertHTMLEqual(payload['content'], content)
first_update_url = reverse('course_info_json', first_update_url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': payload['id']}) 'provided_id': payload['id']})
content += '<div>div <p>p<br/></p></div>' content += '<div>div <p>p<br/></p></div>'
payload['content'] = content payload['content'] = content
resp = self.client.post(first_update_url, json.dumps(payload), resp = self.client.post(first_update_url, json.dumps(payload),
"application/json") "application/json")
self.assertHTMLEqual(content, json.loads(resp.content)['content'], self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div") "iframe w/ div")
# now put in an evil update # now put in an evil update
content = '<ol/>' content = '<ol/>'
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
...@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase): ...@@ -58,25 +58,24 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(content, payload['content'], "self closing ol") self.assertHTMLEqual(content, payload['content'], "self closing ol")
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.get(url) resp = self.client.get(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2) self.assertTrue(len(payload) == 2)
# can't test non-json paylod b/c expect_json throws error # can't test non-json paylod b/c expect_json throws error
# try json w/o required fields # try json w/o required fields
self.assertContains( self.assertContains(self.client.post(url, json.dumps({'garbage': 1}),
self.client.post(url, json.dumps({'garbage': 1}), "application/json"),
"application/json"), 'Failed to save', status_code=400)
'Failed to save', status_code=400)
# now try to update a non-existent update # now try to update a non-existent update
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': '9'}) 'provided_id': '9'})
content = 'blah blah' content = 'blah blah'
payload = {'content': content, payload = {'content': content,
'date': 'January 21, 2013'} 'date': 'January 21, 2013'}
...@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -89,8 +88,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
self.assertContains( self.assertContains(
self.client.post(url, json.dumps(payload), "application/json"), self.client.post(url, json.dumps(payload), "application/json"),
...@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -101,8 +100,8 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 11, 2013'} 'date': 'January 11, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content) payload = json.loads(resp.content)
...@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase): ...@@ -110,8 +109,8 @@ class CourseUpdateTest(CourseTestCase):
# now try to delete a non-existent update # now try to delete a non-existent update
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': '19'}) 'provided_id': '19'})
payload = {'content': content, payload = {'content': content,
'date': 'January 21, 2013'} 'date': 'January 21, 2013'}
self.assertContains(self.client.delete(url), "delete", status_code=400) self.assertContains(self.client.delete(url), "delete", status_code=400)
...@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase): ...@@ -121,25 +120,25 @@ class CourseUpdateTest(CourseTestCase):
payload = {'content': content, payload = {'content': content,
'date': 'January 28, 2013'} 'date': 'January 28, 2013'}
url = reverse('course_info_json', kwargs={'org': self.course_location.org, url = reverse('course_info_json', kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.post(url, json.dumps(payload), "application/json") resp = self.client.post(url, json.dumps(payload), "application/json")
payload = json.loads(resp.content) payload = json.loads(resp.content)
this_id = payload['id'] this_id = payload['id']
self.assertHTMLEqual(content, payload['content'], "single iframe") self.assertHTMLEqual(content, payload['content'], "single iframe")
# first count the entries # first count the entries
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': ''}) 'provided_id': ''})
resp = self.client.get(url) resp = self.client.get(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
before_delete = len(payload) before_delete = len(payload)
url = reverse('course_info_json', url = reverse('course_info_json',
kwargs={'org': self.course_location.org, kwargs={'org': self.course_location.org,
'course': self.course_location.course, 'course': self.course_location.course,
'provided_id': this_id}) 'provided_id': this_id})
resp = self.client.delete(url) resp = self.client.delete(url)
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertTrue(len(payload) == before_delete - 1) self.assertTrue(len(payload) == before_delete - 1)
...@@ -6,6 +6,7 @@ from django.test.client import Client ...@@ -6,6 +6,7 @@ from django.test.client import Client
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
class InternationalizationTest(ModuleStoreTestCase): class InternationalizationTest(ModuleStoreTestCase):
""" """
Tests to validate Internationalization. Tests to validate Internationalization.
...@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -38,34 +39,33 @@ class InternationalizationTest(ModuleStoreTestCase):
'org': 'MITx', 'org': 'MITx',
'number': '999', 'number': '999',
'display_name': 'Robot Super Course', 'display_name': 'Robot Super Course',
} }
def test_course_plain_english(self): def test_course_plain_english(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains(resp, self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="title-1">My Courses</h1>',
status_code=200, status_code=200,
html=True) html=True)
def test_course_explicit_english(self): def test_course_explicit_english(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'), resp = self.client.get(reverse('index'),
{}, {},
HTTP_ACCEPT_LANGUAGE='en' HTTP_ACCEPT_LANGUAGE='en'
) )
self.assertContains(resp, self.assertContains(resp,
'<h1 class="title-1">My Courses</h1>', '<h1 class="title-1">My Courses</h1>',
status_code=200, status_code=200,
html=True) html=True)
# **** # ****
# NOTE: # NOTE:
...@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -74,14 +74,13 @@ class InternationalizationTest(ModuleStoreTestCase):
# This test will break when we replace this fake 'test' language # This test will break when we replace this fake 'test' language
# with actual French. This test will need to be updated with # with actual French. This test will need to be updated with
# actual French at that time. # actual French at that time.
# Test temporarily disable since it depends on creation of dummy strings # Test temporarily disable since it depends on creation of dummy strings
@skip @skip
def test_course_with_accents (self): def test_course_with_accents(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
self.client = Client() self.client = Client()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get(reverse('index'), resp = self.client.get(reverse('index'),
{}, {},
HTTP_ACCEPT_LANGUAGE='fr' HTTP_ACCEPT_LANGUAGE='fr'
...@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -90,8 +89,8 @@ class InternationalizationTest(ModuleStoreTestCase):
TEST_STRING = u'<h1 class="title-1">' \ TEST_STRING = u'<h1 class="title-1">' \
+ u'My \xc7\xf6\xfcrs\xe9s L#' \ + u'My \xc7\xf6\xfcrs\xe9s L#' \
+ u'</h1>' + u'</h1>'
self.assertContains(resp, self.assertContains(resp,
TEST_STRING, TEST_STRING,
status_code=200, status_code=200,
html=True) html=True)
# pylint: disable=W0401, W0511
# TODO: component.py should explicitly enumerate exports with __all__
from .component import *
# TODO: course.py should explicitly enumerate exports with __all__
from .course import *
# Disable warnings about import from wildcard
# All files below declare exports with __all__
from .assets import *
from .checklist import *
from .error import *
from .item import *
from .preview import *
from .public import *
from .user import *
from .tabs import *
from .requests import *
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from django.core.exceptions import PermissionDenied
from ..utils import get_course_location_for_item
def get_location_and_verify_access(request, org, course, name):
"""
Create the location tuple verify that the user has permissions
to view the location. Returns the location.
"""
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()
return location
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS,
I'm presuming that the course instructor (formally known as admin)
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
return _has_access
import logging
import json
import os
import tarfile
import shutil
from tempfile import mkdtemp
from path import path
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.urlresolvers import reverse
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from auth.authz import create_all_course_groups
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from ..utils import get_url_reverse
from .access import get_location_and_verify_access
__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
"""
Display an editable asset library
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
})
course_module = modulestore().get_item(location)
course_reference = StaticContent.compute_location(org, course, name)
assets = contentstore().get_all_content_for_course(course_reference)
# sort in reverse upload date order
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
asset_display = []
for asset in assets:
id = asset['_id']
display_info = {}
display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
_thumbnail_location = asset.get('thumbnail_location', None)
thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
asset_display.append(display_info)
return render_to_response('asset_index.html', {
'active_tab': 'assets',
'context_course': course_module,
'assets': asset_display,
'upload_asset_callback_url': upload_asset_callback_url
})
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
be supported by GridFS in MongoDB.
'''
if request.method != 'POST':
# (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
return HttpResponseBadRequest()
# construct a location from the passed in path
location = get_location_and_verify_access(request, org, course, coursename)
# Does the course actually exist?!? Get anything from it to prove its existance
try:
modulestore().get_item(location)
except:
# no return it as a Bad Request response
logging.error('Could not find course' + location)
return HttpResponseBadRequest()
# compute a 'filename' which is similar to the location formatting, we're using the 'filename'
# nomenclature since we're using a FileSystem paradigm here. We're just imposing
# the Location string formatting expectations to keep things a bit more consistent
filename = request.FILES['file'].name
mime_type = request.FILES['file'].content_type
filedata = request.FILES['file'].read()
content_loc = StaticContent.compute_location(org, course, filename)
content = StaticContent(content_loc, filename, mime_type, filedata)
# first let's see if a thumbnail can be created
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
del_cached_content(thumbnail_location)
# now store thumbnail location only if we could create it
if thumbnail_content is not None:
content.thumbnail_location = thumbnail_location
# then commit the content
contentstore().save(content)
del_cached_content(content.location)
# readback the saved content - we need the database timestamp
readback = contentstore().find(content.location)
response_payload = {'displayname': content.name,
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed'
}
response = HttpResponse(json.dumps(response_payload))
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
@ensure_csrf_cookie
@login_required
def import_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
if request.method == 'POST':
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tf = tarfile.open(temp_filepath)
tf.extractall(course_dir + '/')
# find the 'course.xml' file
for r, d, f in os.walk(course_dir):
for files in f:
if files == 'course.xml':
break
if files == 'course.xml':
break
if files != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(r))
if r != course_dir:
for fname in os.listdir(r):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=Location(location),
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'active_tab': 'import',
'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
# export out to a tempdir
logging.debug('root = {0}'.format(root_dir))
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
#filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz')
tf.add(root_dir / name, arcname=name)
tf.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'active_tab': 'export',
'successful_import_redirect_url': ''
})
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .requests import get_request_method
from .access import get_location_and_verify_access
__all__ = ['get_checklists', 'update_checklist']
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
"""
Send models, views, and html for displaying the course checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
course_module.checklists = template_module.checklists
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
if copied or modified:
modulestore.update_metadata(location, own_metadata(course_module))
return render_to_response('checklists.html',
{
'context_course': course_module,
'checklists': checklists
})
@ensure_csrf_cookie
@login_required
def update_checklist(request, org, course, name, checklist_index=None):
"""
restful CRUD operations on course checklists. The payload is a json rep of
the modified checklist. For PUT or POST requests, the index of the
checklist being modified must be included; the returned payload will
be just that one checklist. For GET requests, the returned payload
is a json representation of the list of all checklists.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
real_method = get_request_method(request)
if real_method == 'POST' or real_method == 'PUT':
if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
index = int(checklist_index)
course_module.checklists[index] = json.loads(request.body)
checklists, modified = expand_checklist_action_urls(course_module)
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
else:
return HttpResponseBadRequest(
"Could not save checklist state because the checklist index was out of range or unspecified.",
content_type="text/plain")
elif request.method == 'GET':
# In the JavaScript view initialize method, we do a fetch to get all the checklists.
checklists, modified = expand_checklist_action_urls(course_module)
if modified:
modulestore.update_metadata(location, own_metadata(course_module))
return HttpResponse(json.dumps(checklists), mimetype="application/json")
else:
return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
def expand_checklist_action_urls(course_module):
"""
Gets the checklists out of the course module and expands their action urls
if they have not yet been expanded.
Returns the checklists with modified urls, as well as a boolean
indicating whether or not the checklists were modified.
"""
checklists = course_module.checklists
modified = False
for checklist in checklists:
if not checklist.get('action_urls_expanded', False):
for item in checklist.get('items'):
item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
checklist['action_urls_expanded'] = True
modified = True
return checklists, modified
from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
def not_found(request):
return render_to_response('error.html', {'error': '404'})
def server_error(request):
return render_to_response('error.html', {'error': '500'})
def render_404(request):
return HttpResponseNotFound(render_to_string('404.html', {}))
def render_500(request):
return HttpResponseServerError(render_to_string('500.html', {}))
import json
from uuid import uuid4
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
__all__ = ['save_item', 'clone_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
@expect_json
def save_item(request):
item_location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
store = get_modulestore(Location(item_location))
if request.POST.get('data') is not None:
data = request.POST['data']
store.update_item(item_location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in request.POST and request.POST['children'] is not None:
children = request.POST['children']
store.update_children(item_location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if request.POST.get('metadata') is not None:
posted_metadata = request.POST['metadata']
# fetch original
existing_item = modulestore().get_item(item_location)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key, value in posted_metadata.items():
if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@login_required
@expect_json
def clone_item(request):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
parent = get_modulestore(template).get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = get_modulestore(template).clone_item(template, dest_location)
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.display_name = display_name
get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@login_required
@expect_json
def delete_item(request):
item_location = request.POST['id']
item_loc = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
raise PermissionDenied()
# optional parameter to delete all children (default False)
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
item = store.get_item(item_location)
if delete_children:
_xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
else:
store.delete_item(item.location, delete_all_versions)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
import logging
import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
import static_replace
from .session_kv_store import SessionKeyValueStore
from .requests import render_from_lms
from .access import has_access
__all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
Dispatch an AJAX action to a preview XModule
Expects a POST request, and passes the arguments to the module
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
dispatch: The action to execute
"""
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError:
log.exception("Module indicating to user that request doesn't exist")
raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except:
log.exception("error processing ajax call")
raise
return HttpResponse(ajax_return)
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
if not has_access(request.user, location):
raise HttpResponseForbidden()
component = modulestore().get_item(location)
return render_to_response('component.html', {
'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
rendering module previews.
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
"""
def preview_model_data(descriptor):
return DbModel(
SessionKeyValueStore(request, descriptor._model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda type, event: None,
filestore=descriptor.system.resources_fs,
get_module=partial(get_preview_module, request, preview_id),
render_template=render_from_lms,
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
xblock_model_data=preview_model_data,
)
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
from the set of preview data for the descriptor specified by Location
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
request: The active django request
preview_id (str): An identifier specifying which preview this module is used for
descriptor: An XModuleDescriptor
instance_state: An instance state string
shared_state: A shared state string
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
return module
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
pair of states returned by get_sample_state() for the supplied descriptor.
descriptor: An XModuleDescriptor
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from .user import index
__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts']
"""
Public views
"""
@ensure_csrf_cookie
def signup(request):
"""
Display the signup form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
"""
Display the login form.
"""
csrf_token = csrf(request)['csrf_token']
return render_to_response('login.html', {
'csrf': csrf_token,
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
def howitworks(request):
if request.user.is_authenticated():
return index(request)
else:
return render_to_response('howitworks.html', {})
def ux_alerts(request):
"""
static/proof-of-concept views
"""
return render_to_response('ux-alerts.html', {})
import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
__all__ = ['edge', 'event', 'landing']
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
# points to the temporary edge page
def edge(request):
return render_to_response('university_profiles/edge.html', {})
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
console logs don't get distracted :-)
'''
return HttpResponse(True)
def get_request_method(request):
"""
Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
what type of request came from the client, and return it.
"""
# 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
return real_method
def create_json_response(errmsg=None):
if errmsg is not None:
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def _xmodule_recurse(item, action):
for child in item.get_children():
_xmodule_recurse(child, action)
action(item)
from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
from access import has_access
from util.json_request import expect_json
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from ..utils import get_course_for_item
from .access import get_location_and_verify_access
__all__ = ['edit_tabs', 'reorder_static_tabs', 'static_pages', 'edit_static']
def initialize_course_tabs(course):
# 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
# at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@login_required
@expect_json
def reorder_static_tabs(request):
tabs = request.POST['tabs']
course = get_course_for_item(tabs[0])
if not has_access(request.user, course.location):
raise PermissionDenied()
# get list of existing static tabs in course
# make sure they are the same lengths (i.e. the number of passed in tabs equals the number
# that we know about) otherwise we can drop some!
existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
if len(existing_static_tabs) != len(tabs):
return HttpResponseBadRequest()
# load all reference tabs, return BadRequest if we can't find any of them
tab_items = []
for tab in tabs:
item = modulestore('direct').get_item(Location(tab))
if item is None:
return HttpResponseBadRequest()
tab_items.append(item)
# now just go through the existing course_tabs and re-order the static tabs
reordered_tabs = []
static_tab_idx = 0
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
reordered_tabs.append(tab)
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@login_required
@ensure_csrf_cookie
def edit_tabs(request, org, course, coursename):
location = ['i4x', org, course, 'course', coursename]
course_item = modulestore().get_item(location)
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
# see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
if course_item.tabs is None or len(course_item.tabs) == 0:
initialize_course_tabs(course_item)
# first get all static tabs from the tabs list
# we do this because this is also the order in which items are displayed in the LMS
static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
static_tabs = []
for static_tab_ref in static_tabs_refs:
static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
static_tabs.append(modulestore('direct').get_item(static_tab_loc))
components = [
static_tab.location.url()
for static_tab
in static_tabs
]
return render_to_response('edit-tabs.html', {
'active_tab': 'pages',
'context_course': course_item,
'components': components
})
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
location = get_location_and_verify_access(request, org, course, coursename)
course = modulestore().get_item(location)
return render_to_response('static-pages.html', {
'active_tab': 'pages',
'context_course': course,
})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_url_reverse, get_lms_link_for_item
from util.json_request import expect_json
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from .access import has_access
from .requests import create_json_response
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last <email@email.com>.
If the first and last names are blank, uses the username instead.
Assumes that the email is not blank.
'''
f = user.first_name
l = user.last_name
if f == '' and l == '':
f = user.username
return '{first} {last} <{email}>'.format(first=f,
last=l,
email=user.email)
@login_required
@ensure_csrf_cookie
def index(request):
"""
List all courses available to the logged in user
"""
courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
and course.location.name != '')
courses = filter(course_filter, courses)
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
for course in courses],
'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
})
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
'''
This view will return all CMS users who are editors for the specified course
'''
# check that logged in user has permissions to this item
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
raise PermissionDenied()
course_module = modulestore().get_item(location)
return render_to_response('manage_users.html', {
'active_tab': 'users',
'context_course': course_module,
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
'request_user_id': request.user.id
})
@expect_json
@login_required
@ensure_csrf_cookie
def add_user(request, location):
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
'''
email = request.POST["email"]
if email == '':
return create_json_response('Please specify an email address.')
# check that logged in user has admin permissions to this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
# user doesn't exist?!? Return error.
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# user exists, but hasn't activated account?!?
if not user.is_active:
return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
# ok, we're cool to add to the course group
add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
@expect_json
@login_required
@ensure_csrf_cookie
def remove_user(request, location):
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
'''
email = request.POST["email"]
# check that logged in user has admin permissions on this course
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
raise PermissionDenied()
user = get_user_by_email(email)
if user is None:
return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
# make sure we're not removing ourselves
if user.id == request.user.id:
raise PermissionDenied()
remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
return create_json_response()
...@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES'] ...@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
#Email overrides
DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
#Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value MITX_FEATURES[feature] = value
......
...@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico') ...@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org' DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org' DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
SERVER_EMAIL = 'devops@edx.org'
ADMINS = ( ADMINS = (
('edX Admins', 'admin@edx.org'), ('edX Admins', 'admin@edx.org'),
) )
......
...@@ -895,4 +895,4 @@ function saveSetSectionScheduleDate(e) { ...@@ -895,4 +895,4 @@ function saveSetSectionScheduleDate(e) {
hideModal(); hideModal();
}); });
} }
\ No newline at end of file
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
* Create a HesitateEvent and assign it as the event to execute: * Create a HesitateEvent and assign it as the event to execute:
* $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger); * $(el).on('mouseEnter', CMS.HesitateEvent( expand, 'mouseLeave').trigger);
* It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event * It calls the executeOnTimeOut function with the event.currentTarget after the configurable timeout IFF the cancelSelector event
* did not occur on the event.currentTarget. * did not occur on the event.currentTarget.
* *
* More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer * More specifically, when trigger is called (triggered by the event you bound it to), it starts a timer
* which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function * which the cancelSelector event will cancel or if the timer finished, it executes the executeOnTimeOut function
* passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your * passing it the original event (whose currentTarget s/b the specific ele). It never accumulates events; however, it doesn't hurt for your
* code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such. * code to minimize invocations of trigger by binding to mouseEnter v mouseOver and such.
* *
* NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything); * NOTE: if something outside of this wants to cancel the event, invoke cachedhesitation.untrigger(null | anything);
*/ */
...@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800; ...@@ -25,7 +25,7 @@ CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) { CMS.HesitateEvent.prototype.trigger = function(event) {
if (event.data.timeoutEventId == null) { if (event.data.timeoutEventId == null) {
event.data.timeoutEventId = window.setTimeout( event.data.timeoutEventId = window.setTimeout(
function() { event.data.fireEvent(event); }, function() { event.data.fireEvent(event); },
CMS.HesitateEvent.DURATION); CMS.HesitateEvent.DURATION);
event.data.originalEvent = event; event.data.originalEvent = event;
$(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger); $(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
...@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) { ...@@ -45,4 +45,4 @@ CMS.HesitateEvent.prototype.untrigger = function(event) {
$(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger); $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
} }
event.data.timeoutEventId = null; event.data.timeoutEventId = null;
}; };
\ No newline at end of file
...@@ -80,6 +80,6 @@ $(document).ready(function(){ ...@@ -80,6 +80,6 @@ $(document).ready(function(){
$('section.problem-edit').show(); $('section.problem-edit').show();
return false; return false;
}); });
}); });
// single per course holds the updates and handouts // single per course holds the updates and handouts
CMS.Models.CourseInfo = Backbone.Model.extend({ CMS.Models.CourseInfo = Backbone.Model.extend({
// This model class is not suited for restful operations and is considered just a server side initialized container // This model class is not suited for restful operations and is considered just a server side initialized container
url: '', url: '',
defaults: { defaults: {
"courseId": "", // the location url "courseId": "", // the location url
"updates" : null, // UpdateCollection "updates" : null, // UpdateCollection
"handouts": null // HandoutCollection "handouts": null // HandoutCollection
}, },
idAttribute : "courseId" idAttribute : "courseId"
}); });
// course update -- biggest kludge here is the lack of a real id to map updates to originals // course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({ CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: { defaults: {
...@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({ ...@@ -26,11 +26,11 @@ CMS.Models.CourseUpdate = Backbone.Model.extend({
*/ */
CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
url : function() {return this.urlbase + "course_info/updates/";}, url : function() {return this.urlbase + "course_info/updates/";},
model : CMS.Models.CourseUpdate model : CMS.Models.CourseUpdate
}); });
\ No newline at end of file
...@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({ ...@@ -16,7 +16,7 @@ CMS.Models.Location = Backbone.Model.extend({
}, },
_tagPattern : /[^:]+/g, _tagPattern : /[^:]+/g,
_fieldPattern : new RegExp('[^/]+','g'), _fieldPattern : new RegExp('[^/]+','g'),
parse: function(payload) { parse: function(payload) {
if (_.isArray(payload)) { if (_.isArray(payload)) {
return { return {
...@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({ ...@@ -25,7 +25,7 @@ CMS.Models.Location = Backbone.Model.extend({
course: payload[2], course: payload[2],
category: payload[3], category: payload[3],
name: payload[4] name: payload[4]
} };
} }
else if (_.isString(payload)) { else if (_.isString(payload)) {
this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes this._tagPattern.lastIndex = 0; // odd regex behavior requires this to be reset sometimes
...@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({ ...@@ -65,4 +65,4 @@ CMS.Models.CourseRelative = Backbone.Model.extend({
CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({ CMS.Models.CourseRelativeCollection = Backbone.Collection.extend({
model : CMS.Models.CourseRelative model : CMS.Models.CourseRelative
}); });
\ No newline at end of file
...@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({ ...@@ -6,5 +6,5 @@ CMS.Models.ModuleInfo = Backbone.Model.extend({
"data": null, "data": null,
"metadata" : null, "metadata" : null,
"children" : null "children" : null
}, }
}); });
\ No newline at end of file
...@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ ...@@ -11,7 +11,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
validate: function (attrs) { validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values. // Keys can no longer be edited. We are currently not validating values.
}, },
save : function (attrs, options) { save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked // wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
...@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({ ...@@ -23,7 +23,7 @@ CMS.Models.Settings.Advanced = Backbone.Model.extend({
}; };
Backbone.Model.prototype.save.call(this, attrs, options); Backbone.Model.prototype.save.call(this, attrs, options);
}, },
afterSave : function(self) { afterSave : function(self) {
// remove deleted attrs // remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) { if (!_.isEmpty(self.deleteKeys)) {
......
...@@ -66,7 +66,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -66,7 +66,7 @@ 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.save({'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.save('intro_video', newsource); if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
......
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object(); if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
defaults : { defaults : {
course_location : null, course_location : null,
graders : null, // CourseGraderCollection graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...} grace_period : null // either null or { hours: n, minutes: m, ...}
}, },
...@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({ ...@@ -54,7 +54,7 @@ CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
"type" : "", // must be unique w/in collection (ie. w/in course) "type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 1, "min_count" : 1,
"drop_count" : 0, "drop_count" : 0,
"short_label" : "", // what to use in place of type if space is an issue "short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100 "weight" : 0 // int 0..100
}, },
parse : function(attrs) { parse : function(attrs) {
...@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({ ...@@ -125,4 +125,4 @@ CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
sumWeights : function() { sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0); return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
} }
}); });
\ No newline at end of file
...@@ -93,4 +93,4 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -93,4 +93,4 @@ CMS.Views.Checklists = Backbone.View.extend({
error : CMS.ServerError error : CMS.ServerError
}); });
} }
}); });
\ No newline at end of file
...@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -32,7 +32,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"click .post-actions > .edit-button" : "onEdit", "click .post-actions > .edit-button" : "onEdit",
"click .post-actions > .delete-button" : "onDelete" "click .post-actions > .delete-button" : "onDelete"
}, },
initialize: function() { initialize: function() {
var self = this; var self = this;
// instantiates an editor template for each update in the collection // instantiates an editor template for each update in the collection
...@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -41,13 +41,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
"/static/client_templates/course_info_update.html", "/static/client_templates/course_info_update.html",
function (raw_template) { function (raw_template) {
self.template = _.template(raw_template); self.template = _.template(raw_template);
self.render(); self.render();
} }
); );
// when the client refetches the updates as a whole, re-render them // when the client refetches the updates as a whole, re-render them
this.listenTo(this.collection, 'reset', this.render); this.listenTo(this.collection, 'reset', this.render);
}, },
render: function () { render: function () {
// iterate over updates and create views for each using the template // iterate over updates and create views for each using the template
var updateEle = this.$el.find("#course-update-list"); var updateEle = this.$el.find("#course-update-list");
...@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -66,14 +66,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' }); this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this; return this;
}, },
onNew: function(event) { onNew: function(event) {
event.preventDefault(); event.preventDefault();
var self = this; var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr // create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate(); var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0}); this.collection.add(newModel, {at : 0});
var $newForm = $(this.template({ updateModel : newModel })); var $newForm = $(this.template({ updateModel : newModel }));
var updateEle = this.$el.find("#course-update-list"); var updateEle = this.$el.find("#course-update-list");
...@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -87,7 +87,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
lineWrapping: true, lineWrapping: true,
}); });
} }
$newForm.addClass('editing'); $newForm.addClass('editing');
this.$currentPost = $newForm.closest('li'); this.$currentPost = $newForm.closest('li');
...@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -99,21 +99,21 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$('.date').datepicker('destroy'); $('.date').datepicker('destroy');
$('.date').datepicker({ 'dateFormat': 'MM d, yy' }); $('.date').datepicker({ 'dateFormat': 'MM d, yy' });
}, },
onSave: function(event) { onSave: function(event) {
event.preventDefault(); event.preventDefault();
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() }); targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change // push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError}); targetModel.save({}, {error : CMS.ServerError});
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Update', { analytics.track('Saved Course Update', {
'course': course_location_analytics, 'course': course_location_analytics,
'date': this.dateEntry(event).val() 'date': this.dateEntry(event).val()
}); });
}, },
onCancel: function(event) { onCancel: function(event) {
event.preventDefault(); event.preventDefault();
// change editor contents back to model values and hide the editor // change editor contents back to model values and hide the editor
...@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -121,13 +121,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id); this.closeEditor(this, !targetModel.id);
}, },
onEdit: function(event) { onEdit: function(event) {
event.preventDefault(); event.preventDefault();
var self = this; var self = this;
this.$currentPost = $(event.target).closest('li'); this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing'); this.$currentPost.addClass('editing');
$(this.editor(event)).show(); $(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first(); var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) { if (this.$codeMirror == null ) {
...@@ -154,13 +154,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -154,13 +154,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
analytics.track('Deleted Course Update', { analytics.track('Deleted Course Update', {
'course': course_location_analytics, 'course': course_location_analytics,
'date': this.dateEntry(event).val() 'date': this.dateEntry(event).val()
}); });
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
targetModel.destroy({success : function (model, response) { targetModel.destroy({success : function (model, response) {
cacheThis.collection.fetch({success : function() {cacheThis.render();}, cacheThis.collection.fetch({success : function() {cacheThis.render();},
error : CMS.ServerError}); error : CMS.ServerError});
}, },
...@@ -192,17 +192,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -192,17 +192,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
this.$codeMirror = null; this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove(); self.$currentPost.find('.CodeMirror').remove();
}, },
// Dereferencing from events to screen elements // Dereferencing from events to screen elements
eventModel: function(event) { eventModel: function(event) {
// not sure if it should be currentTarget or delegateTarget // not sure if it should be currentTarget or delegateTarget
return this.collection.get($(event.currentTarget).attr("name")); return this.collection.get($(event.currentTarget).attr("name"));
}, },
modelDom: function(event) { modelDom: function(event) {
return $(event.currentTarget).closest("li"); return $(event.currentTarget).closest("li");
}, },
editor: function(event) { editor: function(event) {
var li = $(event.currentTarget).closest("li"); var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("form").first(); if (li) return $(li).find("form").first();
...@@ -216,7 +216,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -216,7 +216,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentEntry: function(event) { contentEntry: function(event) {
return $(event.currentTarget).closest("li").find(".new-update-content").first(); return $(event.currentTarget).closest("li").find(".new-update-content").first();
}, },
dateDisplay: function(event) { dateDisplay: function(event) {
return $(event.currentTarget).closest("li").find("#date-display").first(); return $(event.currentTarget).closest("li").find("#date-display").first();
}, },
...@@ -224,7 +224,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -224,7 +224,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
contentDisplay: function(event) { contentDisplay: function(event) {
return $(event.currentTarget).closest("li").find(".update-contents").first(); return $(event.currentTarget).closest("li").find(".update-contents").first();
} }
}); });
// the handouts view is dumb right now; it needs tied to a model and all that jazz // the handouts view is dumb right now; it needs tied to a model and all that jazz
...@@ -245,7 +245,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -245,7 +245,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
"/static/client_templates/course_info_handouts.html", "/static/client_templates/course_info_handouts.html",
function (raw_template) { function (raw_template) {
self.template = _.template(raw_template); self.template = _.template(raw_template);
self.render(); self.render();
} }
); );
}, },
...@@ -253,8 +253,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -253,8 +253,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
} }
); );
}, },
render: function () { render: function () {
var updateEle = this.$el; var updateEle = this.$el;
var self = this; var self = this;
this.$el.html( this.$el.html(
...@@ -313,4 +313,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -313,4 +313,4 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
self.$form.find('.CodeMirror').remove(); self.$form.find('.CodeMirror').remove();
this.$codeMirror = null; this.$codeMirror = null;
} }
}); });
\ No newline at end of file
...@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({ ...@@ -16,7 +16,7 @@ CMS.Models.AssignmentGrade = Backbone.Model.extend({
urlRoot : function() { urlRoot : function() {
if (this.has('location')) { if (this.has('location')) {
var location = this.get('location'); var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/' return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/'
+ location.get('name') + '/gradeas/'; + location.get('name') + '/gradeas/';
} }
else return ""; else return "";
...@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ ...@@ -37,14 +37,14 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
'<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' + '<a data-tooltip="Mark/unmark this subsection as graded" class="menu-toggle" href="#">' +
'<% if (!hideSymbol) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' + '<% if (!hideSymbol) {%><span class="ss-icon ss-standard">&#x2713;</span><%};%>' +
'</a>' + '</a>' +
'<ul class="menu">' + '<ul class="menu">' +
'<% graders.each(function(option) { %>' + '<% graders.each(function(option) { %>' +
'<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' + '<li><a <% if (option.get("type") == assignmentType) {%>class="is-selected" <%}%> href="#"><%= option.get("type") %></a></li>' +
'<% }) %>' + '<% }) %>' +
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' + '<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>'); '</ul>');
this.assignmentGrade = new CMS.Models.AssignmentGrade({ this.assignmentGrade = new CMS.Models.AssignmentGrade({
assignmentUrl : this.$el.closest('.id-holder').data('id'), assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')}); graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null // TODO throw exception if graders is null
this.graders = this.options['graders']; this.graders = this.options['graders'];
...@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({ ...@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
}, },
selectGradeType : function(e) { selectGradeType : function(e) {
e.preventDefault(); e.preventDefault();
this.removeMenu(e); this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr // TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly) // of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text()); this.assignmentGrade.save('graderType', $(e.target).text());
this.render(); this.render();
} }
}) })
\ No newline at end of file
...@@ -6,26 +6,26 @@ $(document).ready(function() { ...@@ -6,26 +6,26 @@ $(document).ready(function() {
$('.unit').draggable({ $('.unit').draggable({
axis: 'y', axis: 'y',
handle: '.drag-handle', handle: '.drag-handle',
zIndex: 999, zIndex: 999,
start: initiateHesitate, start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down // left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future // to work in the future
drag: generateCheckHoverState('.collapsed', ''), drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate, stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
// Subsection reordering // Subsection reordering
$('.id-holder').draggable({ $('.id-holder').draggable({
axis: 'y', axis: 'y',
handle: '.section-item .drag-handle', handle: '.section-item .drag-handle',
zIndex: 999, zIndex: 999,
start: initiateHesitate, start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''), drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate, stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
// Section reordering // Section reordering
$('.courseware-section').draggable({ $('.courseware-section').draggable({
axis: 'y', axis: 'y',
...@@ -33,8 +33,8 @@ $(document).ready(function() { ...@@ -33,8 +33,8 @@ $(document).ready(function() {
stack: '.courseware-section', stack: '.courseware-section',
revert: "invalid" revert: "invalid"
}); });
$('.sortable-unit-list').droppable({ $('.sortable-unit-list').droppable({
accept : '.unit', accept : '.unit',
greedy: true, greedy: true,
...@@ -50,7 +50,7 @@ $(document).ready(function() { ...@@ -50,7 +50,7 @@ $(document).ready(function() {
drop: onSubsectionReordered, drop: onSubsectionReordered,
greedy: true greedy: true
}); });
// Section reordering // Section reordering
$('.courseware-overview').droppable({ $('.courseware-overview').droppable({
accept : '.courseware-section', accept : '.courseware-section',
...@@ -58,7 +58,7 @@ $(document).ready(function() { ...@@ -58,7 +58,7 @@ $(document).ready(function() {
drop: onSectionReordered, drop: onSectionReordered,
greedy: true greedy: true
}); });
// stop clicks on drag bars from doing their thing w/o stopping drag // stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); }); $('.drag-handle').click(function(e) {e.preventDefault(); });
...@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) { ...@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) {
$.extend(droppable, {offset : $(droppable).offset()}); $.extend(droppable, {offset : $(droppable).offset()});
var t = droppable.offset.top, var t = droppable.offset.top,
b = t + droppable.proportions.height; b = t + droppable.proportions.height;
if (t === b) { if (t === b) {
...@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
this[c === "isout" ? "isover" : "isout"] = false; this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave"); $(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
}); });
$(selectorsToShove).each(function() { $(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top); var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
if ($(this).hasClass('ui-dragging-pushup')) { if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) { if (!intersectsBottom) {
console.log('not up', $(this).data('id')); console.log('not up', $(this).data('id'));
...@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('up', $(this).data('id')); console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup'); $(this).addClass('ui-dragging-pushup');
} }
var intersectsTop = computeIntersection(this, ui.helper, var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height); (draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
if ($(this).hasClass('ui-dragging-pushdown')) { if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) { if (!intersectsTop) {
console.log('not down', $(this).data('id')); console.log('not down', $(this).data('id'));
...@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) { ...@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('down', $(this).data('id')); console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown'); $(this).addClass('ui-dragging-pushdown');
} }
}); });
} }
} }
...@@ -159,20 +159,20 @@ function removeHesitate(event, ui) { ...@@ -159,20 +159,20 @@ function removeHesitate(event, ui) {
} }
function expandSection(event) { function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed', 400); $(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon // don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse'); $(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
} }
function onUnitReordered(event, ui) { function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection, // a unit's been dropped on this subsection,
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf'); _handleReorder(event, ui, 'subsection-id', 'li:.leaf');
} }
function onSubsectionReordered(event, ui) { function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section, // a subsection has been dropped on this section,
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch'); _handleReorder(event, ui, 'section-id', 'li:.branch');
} }
...@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) { ...@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) {
} }
function _handleReorder(event, ui, parentIdField, childrenSelector) { function _handleReorder(event, ui, parentIdField, childrenSelector) {
// figure out where it came from and where it slots in. // figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField); var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector); var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get(); var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
......
CMS.ServerError = function(model, error) { CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches // this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText); window.alert("Server Error: " + error.responseText);
}; };
\ No newline at end of file
CMS.Views.ValidatingView = Backbone.View.extend({ CMS.Views.ValidatingView = Backbone.View.extend({
// Intended as an abstract class which catches validation errors on the model and // Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how // decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents // either have your init call this one or copy the contents
initialize : function() { initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError); this.listenTo(this.model, 'error', CMS.ServerError);
...@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({ ...@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
"change 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
// which may be the subjects of validation errors // which may be the subjects of validation errors
}, },
_cacheValidationErrors : [], _cacheValidationErrors : [],
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">Server Error</%block>
<%block name="title">Studio Server Error</%block>
<%block name="content"> <%block name="content">
<div class="wrapper-content wrapper"> <div class="wrapper-content wrapper">
<section class="content"> <section class="content">
<h1>Currently the <em>edX</em> servers are down</h1> <h1>The <em>Studio</em> servers encountered an error</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p> <p>
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at <a href="mailto:technical@edx.org">technical@edx.org</a>.
</p>
</section> </section>
</div> </div>
......
...@@ -137,4 +137,4 @@ def clear_courses(): ...@@ -137,4 +137,4 @@ def clear_courses():
# $ mongo test_xmodule --eval "db.dropDatabase()" # $ mongo test_xmodule --eval "db.dropDatabase()"
_MODULESTORES = {} _MODULESTORES = {}
modulestore().collection.drop() modulestore().collection.drop()
update_templates() update_templates(modulestore('direct'))
...@@ -1783,7 +1783,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1783,7 +1783,7 @@ class FormulaResponse(LoncapaResponse):
response_tag = 'formularesponse' response_tag = 'formularesponse'
hint_tag = 'formulahint' hint_tag = 'formulahint'
allowed_inputfields = ['textline'] allowed_inputfields = ['textline']
required_attributes = ['answer'] required_attributes = ['answer', 'samples']
max_inputfields = 1 max_inputfields = 1
def setup_response(self): def setup_response(self):
......
...@@ -52,6 +52,7 @@ setup( ...@@ -52,6 +52,7 @@ setup(
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor", "annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor", "foldit = xmodule.foldit_module:FolditDescriptor",
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor", "hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor", "raw = xmodule.raw_module:RawDescriptor",
], ],
......
...@@ -104,11 +104,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -104,11 +104,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': js = {
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'), 'coffee':
resource_string(__name__, 'js/src/collapsible.coffee'), [
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
]} resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
]
}
js_module_name = "CombinedOpenEnded" js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
......
...@@ -382,6 +382,19 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -382,6 +382,19 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return definition, children return definition, children
def definition_to_xml(self, resource_fs):
xml_object = super(CourseDescriptor, self).definition_to_xml(resource_fs)
if len(self.textbooks) > 0:
textbook_xml_object = etree.Element('textbook')
for textbook in self.textbooks:
textbook_xml_object.set('title', textbook.title)
textbook_xml_object.set('book_url', textbook.book_url)
xml_object.append(textbook_xml_object)
return xml_object
def has_ended(self): def has_ended(self):
""" """
Returns True if the current time is after the specified course end date. Returns True if the current time is after the specified course end date.
......
.input-cloud {
margin: 5px;
}
.result_cloud_section {
display: none;
width: 0px;
height: 0px;
}
.result_cloud_section.active {
display: block;
width: 635px;
height: auto;
margin-left: auto;
margin-right: auto;
}
.your_words{
font-size: 0.85em;
display: block;
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('logme', [], function () {
var debugMode;
// debugMode can be one of the following:
//
// true - All messages passed to logme will be written to the internal
// browser console.
// false - Suppress all output to the internal browser console.
//
// Obviously, if anywhere there is a direct console.log() call, we can't do
// anything about it. That's why use logme() - it will allow to turn off
// the output of debug information with a single change to a variable.
debugMode = true;
return logme;
/*
* function: logme
*
* A helper function that provides logging facilities. We don't want
* to call console.log() directly, because sometimes it is not supported
* by the browser. Also when everything is routed through this function.
* the logging output can be easily turned off.
*
* logme() supports multiple parameters. Each parameter will be passed to
* console.log() function separately.
*
*/
function logme() {
var i;
if (
(typeof debugMode === 'undefined') ||
(debugMode !== true) ||
(typeof window.console === 'undefined')
) {
return;
}
for (i = 0; i < arguments.length; i++) {
window.console.log(arguments[i]);
}
} // End-of: function logme
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
window.WordCloud = function (el) {
RequireJS.require(['WordCloudMain'], function (WordCloudMain) {
new WordCloudMain(el);
});
};
...@@ -476,7 +476,15 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -476,7 +476,15 @@ class MongoModuleStore(ModuleStoreBase):
''' '''
# TODO (vshnayder): Why do I have to specify i4x here? # TODO (vshnayder): Why do I have to specify i4x here?
course_filter = Location("i4x", category="course") course_filter = Location("i4x", category="course")
return self.get_items(course_filter) return [
course
for course
in self.get_items(course_filter)
if not (
course.location.org == 'edx' and
course.location.course == 'templates'
)
]
def _find_one(self, location): def _find_one(self, location):
'''Look for a given location in the collection. If revision is not '''Look for a given location in the collection. If revision is not
......
...@@ -42,7 +42,7 @@ class ModuleStoreTestCase(TestCase): ...@@ -42,7 +42,7 @@ class ModuleStoreTestCase(TestCase):
num_templates = modulestore.collection.find(query).count() num_templates = modulestore.collection.find(query).count()
if num_templates < 1: if num_templates < 1:
update_templates() update_templates(modulestore)
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
......
...@@ -7,6 +7,7 @@ from pprint import pprint ...@@ -7,6 +7,7 @@ from pprint import pprint
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from .test_modulestore import check_path_to_location from .test_modulestore import check_path_to_location
from . import DATA_DIR from . import DATA_DIR
...@@ -45,6 +46,7 @@ class TestMongoModuleStore(object): ...@@ -45,6 +46,7 @@ class TestMongoModuleStore(object):
# Explicitly list the courses to load (don't want the big one) # Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple'] courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses) import_from_xml(store, DATA_DIR, courses)
update_templates(store)
return store return store
@staticmethod @staticmethod
...@@ -103,3 +105,11 @@ class TestMongoModuleStore(object): ...@@ -103,3 +105,11 @@ class TestMongoModuleStore(object):
def test_path_to_location(self): def test_path_to_location(self):
'''Make sure that path_to_location works''' '''Make sure that path_to_location works'''
check_path_to_location(self.store) check_path_to_location(self.store)
def test_get_courses_has_no_templates(self):
courses = self.store.get_courses()
for course in courses:
assert_false(
course.location.org == 'edx' and course.location.course == 'templates',
'{0} is a template course'.format(course)
)
...@@ -294,9 +294,8 @@ class CombinedOpenEndedV1Module(): ...@@ -294,9 +294,8 @@ class CombinedOpenEndedV1Module():
if self.current_task_number > 0: if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1) last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number) current_response_data = self.get_current_attributes(self.current_task_number)
if (current_response_data['min_score_to_attempt'] > last_response_data['score'] if (current_response_data['min_score_to_attempt'] > last_response_data['score']
or current_response_data['max_score_to_attempt'] < last_response_data['score']): or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE self.state = self.DONE
self.ready_to_reset = True self.ready_to_reset = True
...@@ -662,9 +661,10 @@ class CombinedOpenEndedV1Module(): ...@@ -662,9 +661,10 @@ class CombinedOpenEndedV1Module():
return { return {
'success': False, 'success': False,
#This is a student_facing_error #This is a student_facing_error
'error': ('You have attempted this question {0} times. ' 'error': (
'You are only allowed to attempt it {1} times.').format( 'You have attempted this question {0} times. '
self.student_attempts, self.attempts) 'You are only allowed to attempt it {1} times.'
).format(self.student_attempts, self.attempts)
} }
self.state = self.INITIAL self.state = self.INITIAL
self.ready_to_reset = False self.ready_to_reset = False
...@@ -803,6 +803,17 @@ class CombinedOpenEndedV1Module(): ...@@ -803,6 +803,17 @@ class CombinedOpenEndedV1Module():
return progress_object return progress_object
def out_of_sync_error(self, get, msg=''):
"""
return dict out-of-sync error message, and also log.
"""
#This is a dev_facing_error
log.warning("Combined module state out sync. state: %r, get: %r. %s",
self.state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
class CombinedOpenEndedV1Descriptor(): class CombinedOpenEndedV1Descriptor():
""" """
...@@ -849,7 +860,6 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -849,7 +860,6 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')} return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
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('combinedopenended') elt = etree.Element('combinedopenended')
......
...@@ -76,7 +76,6 @@ class GradingService(object): ...@@ -76,7 +76,6 @@ class GradingService(object):
return r.text return r.text
def _try_with_login(self, operation): def _try_with_login(self, operation):
""" """
Call operation(), which should return a requests response object. If Call operation(), which should return a requests response object. If
...@@ -87,7 +86,7 @@ class GradingService(object): ...@@ -87,7 +86,7 @@ class GradingService(object):
""" """
response = operation() response = operation()
if (response.json if (response.json
and response.json.get('success') == False and response.json.get('success') is False
and response.json.get('error') == 'login_required'): and response.json.get('error') == 'login_required'):
# apparrently we aren't logged in. Try to fix that. # apparrently we aren't logged in. Try to fix that.
r = self._login() r = self._login()
......
...@@ -72,7 +72,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -72,7 +72,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self._parse(oeparam, self.child_prompt, self.child_rubric, system) self._parse(oeparam, self.child_prompt, self.child_rubric, system)
if self.child_created == True and self.child_state == self.ASSESSING: if self.child_created is True and self.child_state == self.ASSESSING:
self.child_created = False self.child_created = False
self.send_to_grader(self.latest_answer(), system) self.send_to_grader(self.latest_answer(), system)
self.child_created = False self.child_created = False
...@@ -159,9 +159,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -159,9 +159,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = int(survey_responses['score']) score = int(survey_responses['score'])
except: except:
#This is a dev_facing_error #This is a dev_facing_error
error_message = ("Could not parse submission id, grader id, " error_message = (
"or feedback from message_post ajax call. Here is the message data: {0}".format( "Could not parse submission id, grader id, "
survey_responses)) "or feedback from message_post ajax call. "
"Here is the message data: {0}".format(survey_responses)
)
log.exception(error_message) log.exception(error_message)
#This is a student_facing_error #This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."} return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
...@@ -179,8 +181,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -179,8 +181,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
queue_name=self.message_queue_name queue_name=self.message_queue_name
) )
student_info = {'anonymous_student_id': anonymous_student_id, student_info = {
'submission_time': qtime, 'anonymous_student_id': anonymous_student_id,
'submission_time': qtime,
} }
contents = { contents = {
'feedback': feedback, 'feedback': feedback,
...@@ -190,8 +193,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -190,8 +193,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'student_info': json.dumps(student_info), 'student_info': json.dumps(student_info),
} }
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(
body=json.dumps(contents)) header=xheader,
body=json.dumps(contents)
)
#Convert error to a success value #Convert error to a success value
success = True success = True
...@@ -224,15 +229,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -224,15 +229,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id + anonymous_student_id +
str(len(self.child_history))) str(len(self.child_history)))
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](), xheader = xqueue_interface.make_xheader(
lms_key=queuekey, lms_callback_url=system.xqueue['construct_callback'](),
queue_name=self.queue_name) lms_key=queuekey,
queue_name=self.queue_name
)
contents = self.payload.copy() contents = self.payload.copy()
# Metadata related to the student submission revealed to the external grader # Metadata related to the student submission revealed to the external grader
student_info = {'anonymous_student_id': anonymous_student_id, student_info = {
'submission_time': qtime, 'anonymous_student_id': anonymous_student_id,
'submission_time': qtime,
} }
#Update contents with student response and student info #Update contents with student response and student info
...@@ -243,12 +251,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -243,12 +251,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
}) })
# Submit request. When successful, 'msg' is the prior length of the queue # Submit request. When successful, 'msg' is the prior length of the queue
(error, msg) = qinterface.send_to_queue(header=xheader, qinterface.send_to_queue(
body=json.dumps(contents)) header=xheader,
body=json.dumps(contents)
)
# State associated with the queueing request # State associated with the queueing request
queuestate = {'key': queuekey, queuestate = {
'time': qtime, } 'key': queuekey,
'time': qtime,
}
return True return True
def _update_score(self, score_msg, queuekey, system): def _update_score(self, score_msg, queuekey, system):
...@@ -302,11 +314,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -302,11 +314,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# We want to display available feedback in a particular order. # We want to display available feedback in a particular order.
# This dictionary specifies which goes first--lower first. # This dictionary specifies which goes first--lower first.
priorities = {# These go at the start of the feedback priorities = {
'spelling': 0, # These go at the start of the feedback
'grammar': 1, 'spelling': 0,
# needs to be after all the other feedback 'grammar': 1,
'markup_text': 3} # needs to be after all the other feedback
'markup_text': 3
}
do_not_render = ['topicality', 'prompt-overlap'] do_not_render = ['topicality', 'prompt-overlap']
default_priority = 2 default_priority = 2
...@@ -393,7 +407,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -393,7 +407,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback = "" rubric_feedback = ""
feedback = self._convert_longform_feedback_to_html(response_items) feedback = self._convert_longform_feedback_to_html(response_items)
rubric_scores = [] rubric_scores = []
if response_items['rubric_scores_complete'] == True: if response_items['rubric_scores_complete'] is True:
rubric_renderer = CombinedOpenEndedRubric(system, True) rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_dict = rubric_renderer.render_rubric(response_items['rubric_xml']) rubric_dict = rubric_renderer.render_rubric(response_items['rubric_xml'])
success = rubric_dict['success'] success = rubric_dict['success']
...@@ -401,8 +415,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -401,8 +415,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores'] rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']: if not response_items['success']:
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR), return system.render_template(
{'errors': feedback}) "{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
{'errors': feedback}
)
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), { feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'], 'grader_type': response_items['grader_type'],
...@@ -496,8 +512,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -496,8 +512,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
grader_types.append(score_result['grader_type']) grader_types.append(score_result['grader_type'])
try: try:
feedback_dict = json.loads(score_result['feedback'][i]) feedback_dict = json.loads(score_result['feedback'][i])
except: except Exception:
pass feedback_dict = score_result['feedback'][i]
feedback_dicts.append(feedback_dict) feedback_dicts.append(feedback_dict)
grader_ids.append(score_result['grader_id'][i]) grader_ids.append(score_result['grader_id'][i])
submission_ids.append(score_result['submission_id']) submission_ids.append(score_result['submission_id'])
...@@ -515,8 +531,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -515,8 +531,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback_items = [feedback] feedback_items = [feedback]
try: try:
feedback_dict = json.loads(score_result['feedback']) feedback_dict = json.loads(score_result['feedback'])
except: except Exception:
pass feedback_dict = score_result.get('feedback', '')
feedback_dicts = [feedback_dict] feedback_dicts = [feedback_dict]
grader_ids = [score_result['grader_id']] grader_ids = [score_result['grader_id']]
submission_ids = [score_result['submission_id']] submission_ids = [score_result['submission_id']]
...@@ -545,8 +561,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -545,8 +561,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not self.child_history: if not self.child_history:
return "" return ""
feedback_dict = self._parse_score_msg(self.child_history[-1].get('post_assessment', ""), system, feedback_dict = self._parse_score_msg(
join_feedback=join_feedback) self.child_history[-1].get('post_assessment', ""),
system,
join_feedback=join_feedback
)
if not short_feedback: if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else '' return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']: if feedback_dict['valid']:
...@@ -711,7 +730,7 @@ class OpenEndedDescriptor(): ...@@ -711,7 +730,7 @@ class OpenEndedDescriptor():
template_dir_name = "openended" template_dir_name = "openended"
def __init__(self, system): def __init__(self, system):
self.system =system self.system = system
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
...@@ -734,8 +753,9 @@ class OpenEndedDescriptor(): ...@@ -734,8 +753,9 @@ class OpenEndedDescriptor():
"""Assumes that xml_object has child k""" """Assumes that xml_object has child k"""
return xml_object.xpath(k)[0] return xml_object.xpath(k)[0]
return {'oeparam': parse('openendedparam')} return {
'oeparam': parse('openendedparam')
}
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.'''
......
...@@ -101,8 +101,9 @@ class OpenEndedChild(object): ...@@ -101,8 +101,9 @@ class OpenEndedChild(object):
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
if system.open_ended_grading_interface: if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(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, self.controller_qs = controller_query_service.ControllerQueryService(
system) system.open_ended_grading_interface,system
)
else: else:
self.peer_gs = MockPeerGradingService() self.peer_gs = MockPeerGradingService()
self.controller_qs = None self.controller_qs = None
......
...@@ -37,7 +37,7 @@ class PeerGradingService(GradingService): ...@@ -37,7 +37,7 @@ class PeerGradingService(GradingService):
def get_next_submission(self, problem_location, grader_id): def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url, response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id}) {'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response)) return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores,
...@@ -100,29 +100,29 @@ without making actual service calls to the grading controller ...@@ -100,29 +100,29 @@ without making actual service calls to the grading controller
class MockPeerGradingService(object): class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id): def get_next_submission(self, problem_location, grader_id):
return json.dumps({'success': True, return {'success': True,
'submission_id': 1, 'submission_id': 1,
'submission_key': "", 'submission_key': "",
'student_response': 'fake student response', 'student_response': 'fake student response',
'prompt': 'fake submission prompt', 'prompt': 'fake submission prompt',
'rubric': 'fake rubric', 'rubric': 'fake rubric',
'max_score': 4}) 'max_score': 4}
def save_grade(self, location, grader_id, submission_id, def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged): score, feedback, submission_key, rubric_scores, submission_flagged):
return json.dumps({'success': True}) return {'success': True}
def is_student_calibrated(self, problem_location, grader_id): def is_student_calibrated(self, problem_location, grader_id):
return json.dumps({'success': True, 'calibrated': True}) return {'success': True, 'calibrated': True}
def show_calibration_essay(self, problem_location, grader_id): def show_calibration_essay(self, problem_location, grader_id):
return json.dumps({'success': True, return {'success': True,
'submission_id': 1, 'submission_id': 1,
'submission_key': '', 'submission_key': '',
'student_response': 'fake student response', 'student_response': 'fake student response',
'prompt': 'fake submission prompt', 'prompt': 'fake submission prompt',
'rubric': 'fake rubric', 'rubric': 'fake rubric',
'max_score': 4}) 'max_score': 4}
def save_calibration_essay(self, problem_location, grader_id, def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score, calibration_essay_id, submission_key, score,
...@@ -130,10 +130,9 @@ class MockPeerGradingService(object): ...@@ -130,10 +130,9 @@ class MockPeerGradingService(object):
return {'success': True, 'actual_score': 2} return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id): def get_problem_list(self, course_id, grader_id):
return json.dumps({'success': True, return {'success': True,
'problem_list': [ 'problem_list': [
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', ]}
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', def get_data_for_location(self, problem_location, student_id):
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}) return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
]})
...@@ -498,7 +498,6 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -498,7 +498,6 @@ class PeerGradingModule(PeerGradingFields, XModule):
log.error("Problem {0} does not exist in this course".format(location)) log.error("Problem {0} does not exist in this course".format(location))
raise raise
for problem in problem_list: for problem in problem_list:
problem_location = problem['location'] problem_location = problem['location']
descriptor = _find_corresponding_module_for_location(problem_location) descriptor = _find_corresponding_module_for_location(problem_location)
......
...@@ -4,8 +4,6 @@ to do set of polls. ...@@ -4,8 +4,6 @@ to do set of polls.
On the client side we show: On the client side we show:
If student does not yet anwered - Question with set of choices. If student does not yet anwered - Question with set of choices.
If student have answered - Question with statistics for each answers. If student have answered - Question with statistics for each answers.
Student can't change his answer.
""" """
import cgi import cgi
......
...@@ -19,7 +19,6 @@ from collections import defaultdict ...@@ -19,7 +19,6 @@ from collections import defaultdict
from .x_module import XModuleDescriptor from .x_module import XModuleDescriptor
from .mako_module import MakoDescriptorSystem from .mako_module import MakoDescriptorSystem
from .modulestore import Location from .modulestore import Location
from .modulestore.django import modulestore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -50,7 +49,7 @@ class TemplateTestSystem(MakoDescriptorSystem): ...@@ -50,7 +49,7 @@ class TemplateTestSystem(MakoDescriptorSystem):
) )
def update_templates(): def update_templates(modulestore):
""" """
Updates the set of templates in the modulestore with all templates currently Updates the set of templates in the modulestore with all templates currently
available from the installed plugins available from the installed plugins
...@@ -58,7 +57,7 @@ def update_templates(): ...@@ -58,7 +57,7 @@ def update_templates():
# cdodge: build up a list of all existing templates. This will be used to determine which # cdodge: build up a list of all existing templates. This will be used to determine which
# templates have been removed from disk - and thus we need to remove from the DB # templates have been removed from disk - and thus we need to remove from the DB
templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None]) templates_to_delete = modulestore.get_items(['i4x', 'edx', 'templates', None, None, None])
for category, templates in all_templates().items(): for category, templates in all_templates().items():
for template in templates: for template in templates:
...@@ -86,9 +85,9 @@ def update_templates(): ...@@ -86,9 +85,9 @@ def update_templates():
), exc_info=True) ), exc_info=True)
continue continue
modulestore('direct').update_item(template_location, template.data) modulestore.update_item(template_location, template.data)
modulestore('direct').update_children(template_location, template.children) modulestore.update_children(template_location, template.children)
modulestore('direct').update_metadata(template_location, template.metadata) modulestore.update_metadata(template_location, template.metadata)
# remove template from list of templates to delete # remove template from list of templates to delete
templates_to_delete = [t for t in templates_to_delete if t.location != template_location] templates_to_delete = [t for t in templates_to_delete if t.location != template_location]
...@@ -97,4 +96,4 @@ def update_templates(): ...@@ -97,4 +96,4 @@ def update_templates():
if len(templates_to_delete) > 0: if len(templates_to_delete) > 0:
logging.debug('deleting dangling templates = {0}'.format(templates_to_delete)) logging.debug('deleting dangling templates = {0}'.format(templates_to_delete))
for template in templates_to_delete: for template in templates_to_delete:
modulestore('direct').delete_item(template.location) modulestore.delete_item(template.location)
---
metadata:
display_name: Word cloud
version: 1
num_inputs: 5
num_top_words: 250
display_student_percents: True
data: {}
children: []
...@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem ...@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem
from mock import Mock from mock import Mock
open_ended_grading_interface = { open_ended_grading_interface = {
'url': 'http://sandbox-grader-001.m.edx.org/peer_grading', 'url': 'blah/',
'username': 'incorrect_user', 'username': 'incorrect_user',
'password': 'incorrect_pass', 'password': 'incorrect_pass',
'staff_grading' : 'staff_grading', 'staff_grading' : 'staff_grading',
...@@ -52,7 +52,7 @@ def test_system(): ...@@ -52,7 +52,7 @@ def test_system():
user=Mock(is_staff=False), user=Mock(is_staff=False),
filestore=Mock(), filestore=Mock(),
debug=True, debug=True,
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10}, xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xblock_model_data=lambda descriptor: descriptor._model_data, xblock_model_data=lambda descriptor: descriptor._model_data,
anonymous_student_id='student', anonymous_student_id='student',
......
import unittest import unittest
from fs.osfs import OSFS from fs.osfs import OSFS
from nose.tools import assert_equals, assert_true
from path import path from path import path
from tempfile import mkdtemp from tempfile import mkdtemp
import shutil import shutil
...@@ -22,9 +21,9 @@ def strip_filenames(descriptor): ...@@ -22,9 +21,9 @@ def strip_filenames(descriptor):
""" """
Recursively strips 'filename' from all children's definitions. Recursively strips 'filename' from all children's definitions.
""" """
print "strip filename from {desc}".format(desc=descriptor.location.url()) print("strip filename from {desc}".format(desc=descriptor.location.url()))
descriptor._model_data.pop('filename', None) descriptor._model_data.pop('filename', None)
if hasattr(descriptor, 'xml_attributes'): if hasattr(descriptor, 'xml_attributes'):
if 'filename' in descriptor.xml_attributes: if 'filename' in descriptor.xml_attributes:
del descriptor.xml_attributes['filename'] del descriptor.xml_attributes['filename']
...@@ -41,12 +40,12 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -41,12 +40,12 @@ class RoundTripTestCase(unittest.TestCase):
''' '''
def check_export_roundtrip(self, data_dir, course_dir): def check_export_roundtrip(self, data_dir, course_dir):
root_dir = path(self.temp_dir) root_dir = path(self.temp_dir)
print "Copying test course to temp dir {0}".format(root_dir) print("Copying test course to temp dir {0}".format(root_dir))
data_dir = path(data_dir) data_dir = path(data_dir)
shutil.copytree(data_dir / course_dir, root_dir / course_dir) shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import" print("Starting import")
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses = initial_import.get_courses() courses = initial_import.get_courses()
...@@ -55,7 +54,7 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -55,7 +54,7 @@ class RoundTripTestCase(unittest.TestCase):
# export to the same directory--that way things like the custom_tags/ folder # export to the same directory--that way things like the custom_tags/ folder
# will still be there. # will still be there.
print "Starting export" print("Starting export")
fs = OSFS(root_dir) fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir) export_fs = fs.makeopendir(course_dir)
...@@ -63,14 +62,14 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -63,14 +62,14 @@ class RoundTripTestCase(unittest.TestCase):
with export_fs.open('course.xml', 'w') as course_xml: with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml) course_xml.write(xml)
print "Starting second import" print("Starting second import")
second_import = XMLModuleStore(root_dir, course_dirs=[course_dir]) second_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
courses2 = second_import.get_courses() courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1) self.assertEquals(len(courses2), 1)
exported_course = courses2[0] exported_course = courses2[0]
print "Checking course equality" print("Checking course equality")
# HACK: filenames change when changing file formats # HACK: filenames change when changing file formats
# during imports from old-style courses. Ignore them. # during imports from old-style courses. Ignore them.
...@@ -81,16 +80,18 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -81,16 +80,18 @@ class RoundTripTestCase(unittest.TestCase):
self.assertEquals(initial_course.id, exported_course.id) self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id course_id = initial_course.id
print "Checking key equality" print("Checking key equality")
self.assertEquals(sorted(initial_import.modules[course_id].keys()), self.assertEquals(sorted(initial_import.modules[course_id].keys()),
sorted(second_import.modules[course_id].keys())) sorted(second_import.modules[course_id].keys()))
print "Checking module equality" print("Checking module equality")
for location in initial_import.modules[course_id].keys(): for location in initial_import.modules[course_id].keys():
print "Checking", location print("Checking", location)
if location.category == 'html': if location.category == 'html':
print ("Skipping html modules--they can't import in" print(
" final form without writing files...") "Skipping html modules--they can't import in"
" final form without writing files..."
)
continue continue
self.assertEquals(initial_import.modules[course_id][location], self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location]) second_import.modules[course_id][location])
...@@ -123,3 +124,6 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -123,3 +124,6 @@ class RoundTripTestCase(unittest.TestCase):
def test_exam_registration_roundtrip(self): def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly # Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "test_exam_registration") self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
def test_word_cloud_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "word_cloud")
...@@ -53,7 +53,7 @@ class BaseCourseTestCase(unittest.TestCase): ...@@ -53,7 +53,7 @@ class BaseCourseTestCase(unittest.TestCase):
def get_course(self, name): def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name) print("Importing {0}".format(name))
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name]) modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses() courses = modulestore.get_courses()
...@@ -145,7 +145,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -145,7 +145,7 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
print descriptor, descriptor._model_data print(descriptor, descriptor._model_data)
self.assertEqual(descriptor.lms.due, Date().from_json(v)) self.assertEqual(descriptor.lms.due, Date().from_json(v))
# Check that the child inherits due correctly # Check that the child inherits due correctly
...@@ -161,7 +161,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -161,7 +161,7 @@ class ImportTestCase(BaseCourseTestCase):
exported_xml = descriptor.export_to_xml(resource_fs) exported_xml = descriptor.export_to_xml(resource_fs)
# Check that the exported xml is just a pointer # Check that the exported xml is just a pointer
print "Exported xml:", exported_xml print("Exported xml:", exported_xml)
pointer = etree.fromstring(exported_xml) pointer = etree.fromstring(exported_xml)
self.assertTrue(is_pointer_tag(pointer)) self.assertTrue(is_pointer_tag(pointer))
# but it's a special case course pointer # but it's a special case course pointer
...@@ -255,29 +255,29 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -255,29 +255,29 @@ class ImportTestCase(BaseCourseTestCase):
no = ["""<html url_name="blah" also="this"/>""", no = ["""<html url_name="blah" also="this"/>""",
"""<html url_name="blah">some text</html>""", """<html url_name="blah">some text</html>""",
"""<problem url_name="blah"><sub>tree</sub></problem>""", """<problem url_name="blah"><sub>tree</sub></problem>""",
"""<course org="HogwartsX" course="Mathemagics" url_name="3.14159"> """<course org="HogwartsX" course="Mathemagics" url_name="3.14159">
<chapter>3</chapter> <chapter>3</chapter>
</course> </course>
"""] """]
for xml_str in yes: for xml_str in yes:
print "should be True for {0}".format(xml_str) print("should be True for {0}".format(xml_str))
self.assertTrue(is_pointer_tag(etree.fromstring(xml_str))) self.assertTrue(is_pointer_tag(etree.fromstring(xml_str)))
for xml_str in no: for xml_str in no:
print "should be False for {0}".format(xml_str) print("should be False for {0}".format(xml_str))
self.assertFalse(is_pointer_tag(etree.fromstring(xml_str))) self.assertFalse(is_pointer_tag(etree.fromstring(xml_str)))
def test_metadata_inherit(self): def test_metadata_inherit(self):
"""Make sure that metadata is inherited properly""" """Make sure that metadata is inherited properly"""
print "Starting import" print("Starting import")
course = self.get_course('toy') course = self.get_course('toy')
def check_for_key(key, node): def check_for_key(key, node):
"recursive check for presence of key" "recursive check for presence of key"
print "Checking {0}".format(node.location.url()) print("Checking {0}".format(node.location.url()))
self.assertTrue(key in node._model_data) self.assertTrue(key in node._model_data)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c) check_for_key(key, c)
...@@ -322,14 +322,14 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -322,14 +322,14 @@ class ImportTestCase(BaseCourseTestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"]) location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location) toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location) two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8") self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8")
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9") self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9")
def test_colon_in_url_name(self): def test_colon_in_url_name(self):
"""Ensure that colons in url_names convert to file paths properly""" """Ensure that colons in url_names convert to file paths properly"""
print "Starting import" print("Starting import")
# Not using get_courses because we need the modulestore object too afterward # Not using get_courses because we need the modulestore object too afterward
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy']) modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
courses = modulestore.get_courses() courses = modulestore.get_courses()
...@@ -337,10 +337,10 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -337,10 +337,10 @@ class ImportTestCase(BaseCourseTestCase):
course = courses[0] course = courses[0]
course_id = course.id course_id = course.id
print "course errors:" print("course errors:")
for (msg, err) in modulestore.get_item_errors(course.location): for (msg, err) in modulestore.get_item_errors(course.location):
print msg print(msg)
print err print(err)
chapters = course.get_children() chapters = course.get_children()
self.assertEquals(len(chapters), 2) self.assertEquals(len(chapters), 2)
...@@ -348,12 +348,12 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -348,12 +348,12 @@ class ImportTestCase(BaseCourseTestCase):
ch2 = chapters[1] ch2 = chapters[1]
self.assertEquals(ch2.url_name, "secret:magic") self.assertEquals(ch2.url_name, "secret:magic")
print "Ch2 location: ", ch2.location print("Ch2 location: ", ch2.location)
also_ch2 = modulestore.get_instance(course_id, ch2.location) also_ch2 = modulestore.get_instance(course_id, ch2.location)
self.assertEquals(ch2, also_ch2) self.assertEquals(ch2, also_ch2)
print "making sure html loaded" print("making sure html loaded")
cloc = course.location cloc = course.location
loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab') loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab')
html = modulestore.get_instance(course_id, loc) html = modulestore.get_instance(course_id, loc)
...@@ -378,11 +378,11 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -378,11 +378,11 @@ class ImportTestCase(BaseCourseTestCase):
for i in (2, 3): for i in (2, 3):
video = sections[i] video = sections[i]
# Name should be 'video_{hash}' # Name should be 'video_{hash}'
print "video {0} url_name: {1}".format(i, video.url_name) print("video {0} url_name: {1}".format(i, video.url_name))
self.assertEqual(len(video.url_name), len('video_') + 12) self.assertEqual(len(video.url_name), len('video_') + 12)
def test_poll_and_conditional_xmodule(self): def test_poll_and_conditional_import(self):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['conditional_and_poll']) modulestore = XMLModuleStore(DATA_DIR, course_dirs=['conditional_and_poll'])
course = modulestore.get_courses()[0] course = modulestore.get_courses()[0]
...@@ -393,10 +393,31 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -393,10 +393,31 @@ class ImportTestCase(BaseCourseTestCase):
self.assertEqual(len(sections), 1) self.assertEqual(len(sections), 1)
location = course.location location = course.location
location = Location(location.tag, location.org, location.course,
'sequential', 'Problem_Demos') conditional_location = Location(
module = modulestore.get_instance(course.id, location) location.tag, location.org, location.course,
self.assertEqual(len(module.children), 2) 'conditional', 'condone'
)
module = modulestore.get_instance(course.id, conditional_location)
self.assertEqual(len(module.children), 1)
poll_location = Location(
location.tag, location.org, location.course,
'poll_question', 'first_poll'
)
module = modulestore.get_instance(course.id, poll_location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.voted, False)
self.assertEqual(module.poll_answer, '')
self.assertEqual(module.poll_answers, {})
self.assertEqual(
module.answers,
[
{'text': u'Yes', 'id': 'Yes'},
{'text': u'No', 'id': 'No'},
{'text': u"Don't know", 'id': 'Dont_know'}
]
)
def test_error_on_import(self): def test_error_on_import(self):
'''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorModule''' '''Check that when load_error_module is false, an exception is raised, rather than returning an ErrorModule'''
...@@ -406,7 +427,6 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -406,7 +427,6 @@ class ImportTestCase(BaseCourseTestCase):
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml) self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
def test_graphicslidertool_import(self): def test_graphicslidertool_import(self):
''' '''
Check to see if definition_from_xml in gst_module.py Check to see if definition_from_xml in gst_module.py
...@@ -423,6 +443,26 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -423,6 +443,26 @@ class ImportTestCase(BaseCourseTestCase):
<plot style="margin-top:15px;margin-bottom:15px;"/>""".strip() <plot style="margin-top:15px;margin-bottom:15px;"/>""".strip()
self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml) self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml)
def test_word_cloud_import(self):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud'])
course = modulestore.get_courses()[0]
chapters = course.get_children()
ch1 = chapters[0]
sections = ch1.get_children()
self.assertEqual(len(sections), 1)
location = course.location
location = Location(
location.tag, location.org, location.course,
'word_cloud', 'cloud1'
)
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5')
self.assertEqual(module.num_top_words, '250')
def test_cohort_config(self): def test_cohort_config(self):
""" """
Check that cohort config parsing works right. Check that cohort config parsing works right.
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test for Xmodule functional logic."""
import json import json
import unittest import unittest
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
class PostData:
"""Class which emulate postdata."""
def __init__(self, dict_data):
self.dict_data = dict_data
def getlist(self, key):
return self.dict_data.get(key)
class LogicTest(unittest.TestCase): class LogicTest(unittest.TestCase):
...@@ -13,15 +24,18 @@ class LogicTest(unittest.TestCase): ...@@ -13,15 +24,18 @@ class LogicTest(unittest.TestCase):
raw_model_data = {} raw_model_data = {}
def setUp(self): def setUp(self):
class EmptyClass: pass class EmptyClass:
pass
self.system = None self.system = None
self.location = None self.location = None
self.descriptor = EmptyClass() self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class self.xmodule_class = self.descriptor_class.module_class
self.xmodule = self.xmodule_class(self.system, self.location, self.xmodule = self.xmodule_class(
self.descriptor, self.raw_model_data) self.system, self.location,
self.descriptor, self.raw_model_data
)
def ajax_request(self, dispatch, get): def ajax_request(self, dispatch, get):
return json.loads(self.xmodule.handle_ajax(dispatch, get)) return json.loads(self.xmodule.handle_ajax(dispatch, get))
...@@ -64,3 +78,42 @@ class ConditionalModuleTest(LogicTest): ...@@ -64,3 +78,42 @@ class ConditionalModuleTest(LogicTest):
html = response['html'] html = response['html']
self.assertEqual(html, []) self.assertEqual(html, [])
class WordCloudModuleTest(LogicTest):
descriptor_class = WordCloudDescriptor
raw_model_data = {
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
'submitted': False
}
def test_bad_ajax_request(self):
# TODO: move top global test. Formalize all our Xmodule errors.
response = self.ajax_request('bad_dispatch', {})
self.assertDictEqual(response, {
'status': 'fail',
'error': 'Unknown Command!'
})
def test_good_ajax_request(self):
post_data = PostData({'student_words[]': ['cat', 'cat', 'dog', 'sun']})
response = self.ajax_request('submit', post_data)
self.assertEqual(response['status'], 'success')
self.assertEqual(response['submitted'], True)
self.assertEqual(response['total_count'], 22)
self.assertDictEqual(
response['student_words'],
{'sun': 1, 'dog': 6, 'cat': 12}
)
self.assertListEqual(
response['top_words'],
[{'text': 'dad', 'size': 2, 'percent': 9.0},
{'text': 'sun', 'size': 1, 'percent': 5.0},
{'text': 'dog', 'size': 6, 'percent': 27.0},
{'text': 'mom', 'size': 1, 'percent': 5.0},
{'text': 'cat', 'size': 12, 'percent': 54.0}]
)
self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) )
import unittest
from xmodule.modulestore import Location
from .import test_system
from test_util_open_ended import MockQueryDict, DummyModulestore
import json
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
import logging
log = logging.getLogger(__name__)
ORG = "edX"
COURSE = "open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
"""
Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
external grading service.
"""
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingSample"])
calibrated_dict = {'location': "blah"}
save_dict = MockQueryDict()
save_dict.update({
'location': "blah",
'submission_id': 1,
'submission_key': "",
'score': 1,
'feedback': "",
'rubric_scores[]': [0, 1],
'submission_flagged': False,
})
def setUp(self):
"""
Create a peer grading module from a test system
@return:
"""
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE)
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
"""
Test if peer grading is closed
@return:
"""
closed = self.peer_grading.closed()
self.assertEqual(closed, False)
def test_get_html(self):
"""
Test to see if the module can be rendered
@return:
"""
html = self.peer_grading.get_html()
def test_get_data(self):
"""
Try getting data from the external grading service
@return:
"""
success, data = self.peer_grading.query_data_for_location()
self.assertEqual(success, True)
def test_get_score(self):
"""
Test getting the score
@return:
"""
score = self.peer_grading.get_score()
self.assertEquals(score['score'], None)
def test_get_max_score(self):
"""
Test getting the max score
@return:
"""
max_score = self.peer_grading.max_score()
self.assertEquals(max_score, None)
def get_next_submission(self):
"""
Test to see if we can get the next mock submission
@return:
"""
success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
self.assertEqual(success, True)
def test_save_grade(self):
"""
Test if we can save the grade
@return:
"""
response = self.peer_grading.save_grade(self.save_dict)
self.assertEqual(response['success'], True)
def test_is_student_calibrated(self):
"""
Check to see if the student has calibrated yet
@return:
"""
calibrated_dict = {'location': "blah"}
response = self.peer_grading.is_student_calibrated(self.calibrated_dict)
self.assertEqual(response['success'], True)
def test_show_calibration_essay(self):
"""
Test showing the calibration essay
@return:
"""
response = self.peer_grading.show_calibration_essay(self.calibrated_dict)
self.assertEqual(response['success'], True)
def test_save_calibration_essay(self):
"""
Test saving the calibration essay
@return:
"""
response = self.peer_grading.save_calibration_essay(self.save_dict)
self.assertEqual(response['success'], True)
def test_peer_grading_problem(self):
"""
See if we can render a single problem
@return:
"""
response = self.peer_grading.peer_grading_problem(self.calibrated_dict)
self.assertEqual(response['success'], True)
def test_get_instance_state(self):
"""
Get the instance state dict
@return:
"""
self.peer_grading.get_instance_state()
class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
"""
Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
external grading service.
"""
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingScored"])
def setUp(self):
"""
Create a peer grading module from a test system
@return:
"""
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE)
def test_metadata_load(self):
peer_grading = self.get_module_from_location(self.problem_location, COURSE)
self.assertEqual(peer_grading.closed(), False)
\ No newline at end of file
from .import test_system
from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.tests.test_export import DATA_DIR
OPEN_ENDED_GRADING_INTERFACE = { OPEN_ENDED_GRADING_INTERFACE = {
'url': 'http://127.0.0.1:3033/', 'url': 'blah/',
'username': 'incorrect', 'username': 'incorrect',
'password': 'incorrect', 'password': 'incorrect',
'staff_grading': 'staff_grading', 'staff_grading': 'staff_grading',
...@@ -11,4 +16,40 @@ S3_INTERFACE = { ...@@ -11,4 +16,40 @@ S3_INTERFACE = {
'aws_access_key': "", 'aws_access_key': "",
'aws_secret_key': "", 'aws_secret_key': "",
"aws_bucket_name": "", "aws_bucket_name": "",
} }
\ No newline at end of file
class MockQueryDict(dict):
"""
Mock a query dict so that it can be used in test classes. This will only work with the combinedopenended tests,
and does not mock the full query dict, only the behavior that is needed there (namely get_list).
"""
def getlist(self, key, default=None):
try:
return super(MockQueryDict, self).__getitem__(key)
except KeyError:
if default is None:
return []
return default
class DummyModulestore(object):
"""
A mixin that allows test classes to have convenience functions to get a module given a location
"""
test_system = test_system()
def setup_modulestore(self, name):
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
courses = self.modulestore.get_courses()
return courses[0]
def get_module_from_location(self, location, course):
course = self.get_course(course)
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
return descriptor.xmodule(self.test_system)
"""Word cloud is ungraded xblock used by students to
generate and view word cloud.
On the client side we show:
If student does not yet anwered - `num_inputs` numbers of text inputs.
If student have answered - words he entered and cloud.
"""
import json
import logging
from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from xmodule.x_module import XModule
from xblock.core import Scope, String, Object, Boolean, List, Integer
log = logging.getLogger(__name__)
def pretty_bool(value):
BOOL_DICT = [True, "True", "true", "T", "t", "1"]
return value in BOOL_DICT
class WordCloudFields(object):
"""XFields for word cloud."""
display_name = String(
help="Display name for this module",
scope=Scope.settings
)
num_inputs = Integer(
help="Number of inputs.",
scope=Scope.settings,
default=5
)
num_top_words = Integer(
help="Number of max words, which will be displayed.",
scope=Scope.settings,
default=250
)
display_student_percents = Boolean(
help="Display usage percents for each word?",
scope=Scope.settings,
default=True
)
# Fields for descriptor.
submitted = Boolean(
help="Whether this student has posted words to the cloud.",
scope=Scope.user_state,
default=False
)
student_words = List(
help="Student answer.",
scope=Scope.user_state,
default=[]
)
all_words = Object(
help="All possible words from all students.",
scope=Scope.content
)
top_words = Object(
help="Top num_top_words words for word cloud.",
scope=Scope.content
)
class WordCloudModule(WordCloudFields, XModule):
"""WordCloud Xmodule"""
js = {
'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
'js': [resource_string(__name__, 'js/src/word_cloud/logme.js'),
resource_string(__name__, 'js/src/word_cloud/d3.min.js'),
resource_string(__name__, 'js/src/word_cloud/d3.layout.cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
}
css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
js_module_name = "WordCloud"
def get_state(self):
"""Return success json answer for client."""
if self.submitted:
total_count = sum(self.all_words.itervalues())
return json.dumps({
'status': 'success',
'submitted': True,
'display_student_percents': pretty_bool(
self.display_student_percents
),
'student_words': {
word: self.all_words[word] for word in self.student_words
},
'total_count': total_count,
'top_words': self.prepare_words(self.top_words, total_count)
})
else:
return json.dumps({
'status': 'success',
'submitted': False,
'display_student_percents': False,
'student_words': {},
'total_count': 0,
'top_words': {}
})
def good_word(self, word):
"""Convert raw word to suitable word."""
return word.strip().lower()
def prepare_words(self, top_words, total_count):
"""Convert words dictionary for client API.
:param top_words: Top words dictionary
:type top_words: dict
:param total_count: Total number of words
:type total_count: int
:rtype: list of dicts. Every dict is 3 keys: text - actual word,
size - counter of word, percent - percent in top_words dataset.
Calculates corrected percents for every top word:
For every word except last, it calculates rounded percent.
For the last is 100 - sum of all other percents.
"""
list_to_return = []
percents = 0
for num, word_tuple in enumerate(top_words.iteritems()):
if num == len(top_words) - 1:
percent = 100 - percents
else:
percent = round(100.0 * word_tuple[1] / total_count)
percents += percent
list_to_return.append(
{
'text': word_tuple[0],
'size': word_tuple[1],
'percent': percent
}
)
return list_to_return
def top_dict(self, dict_obj, amount):
"""Return top words from all words, filtered by number of
occurences
:param dict_obj: all words
:type dict_obj: dict
:param amount: number of words to be in top dict
:type amount: int
:rtype: dict
"""
return dict(
sorted(
dict_obj.items(),
key=lambda x: x[1],
reverse=True
)[:amount]
)
def handle_ajax(self, dispatch, post):
"""Ajax handler.
Args:
dispatch: string request slug
post: dict request get parameters
Returns:
json string
"""
if dispatch == 'submit':
if self.submitted:
return json.dumps({
'status': 'fail',
'error': 'You have already posted your data.'
})
# Student words from client.
# FIXME: we must use raw JSON, not a post data (multipart/form-data)
raw_student_words = post.getlist('student_words[]')
student_words = filter(None, map(self.good_word, raw_student_words))
self.student_words = student_words
# FIXME: fix this, when xblock will support mutable types.
# Now we use this hack.
# speed issues
temp_all_words = self.all_words
self.submitted = True
# Save in all_words.
for word in self.student_words:
temp_all_words[word] = temp_all_words.get(word, 0) + 1
# Update top_words.
self.top_words = self.top_dict(
temp_all_words,
int(self.num_top_words)
)
# Save all_words in database.
self.all_words = temp_all_words
return self.get_state()
elif dispatch == 'get_state':
return self.get_state()
else:
return json.dumps({
'status': 'fail',
'error': 'Unknown Command!'
})
def get_html(self):
"""Template rendering."""
context = {
'element_id': self.location.html_id(),
'element_class': self.location.category,
'ajax_url': self.system.ajax_url,
'num_inputs': int(self.num_inputs),
'submitted': self.submitted
}
self.content = self.system.render_template('word_cloud.html', context)
return self.content
class WordCloudDescriptor(WordCloudFields, RawDescriptor):
"""Descriptor for WordCloud Xmodule."""
module_class = WordCloudModule
template_dir_name = 'word_cloud'
stores_state = True
mako_template = "widgets/raw-edit.html"
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX"/> <course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edX" />
<sequential> <course>
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
<chapter filename="Overview" slug="Overview" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Overview"/> <chapter filename="Overview" slug="Overview" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Overview"/>
<chapter filename="Week_1" slug="Week_1" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1"/> <chapter filename="Week_1" slug="Week_1" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Week 1"/>
<chapter slug="Midterm_Exam" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Midterm Exam"> <chapter slug="Midterm_Exam" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Midterm Exam">
...@@ -9,4 +10,4 @@ ...@@ -9,4 +10,4 @@
<vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/> <vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
</sequential> </sequential>
</chapter> </chapter>
</sequential> </course>
This is a very very simple course, useful for debugging open ended grading code.
<combinedopenended attempts="10000" display_name = "Humanities Question -- Machine Assessed">
<rubric>
<rubric>
<category>
<description>Writing Applications</description>
<option> The essay loses focus, has little information or supporting details, and the organization makes it difficult to follow.</option>
<option> The essay presents a mostly unified theme, includes sufficient information to convey the theme, and is generally organized well.</option>
</category>
<category>
<description> Language Conventions </description>
<option> The essay demonstrates a reasonable command of proper spelling and grammar. </option>
<option> The essay demonstrates superior command of proper spelling and grammar.</option>
</category>
</rubric>
</rubric>
<prompt>
<h4>Censorship in the Libraries</h4>
<p>"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author</p>
<p>Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.</p>
</prompt>
<task>
<selfassessment/>
</task>
<task>
<openended min_score_to_attempt="2" max_score_to_attempt="3">
<openendedparam>
<initial_display>Enter essay here.</initial_display>
<answer_display>This is the answer.</answer_display>
<grader_payload>{"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}</grader_payload>
</openendedparam>
</openended>
</task>
</combinedopenended>
\ No newline at end of file
<course org="edX" course="open_ended" url_name="2012_Fall"/>
<course>
<chapter url_name="Overview">
<combinedopenended url_name="SampleQuestion"/>
<peergrading url_name="PeerGradingSample"/>
<peergrading url_name="PeerGradingScored"/>
</chapter>
</course>
<peergrading/>
\ No newline at end of file
<peergrading is_graded="True" max_grade="1" use_for_single_location="False" link_to_location="i4x://edX/open_ended/combinedopenended/SampleQuestion"/>
\ No newline at end of file
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Self Assessment Test",
"graded": "true"
},
"chapter/Overview": {
"display_name": "Overview"
},
"combinedopenended/SampleQuestion": {
"display_name": "Sample Question"
},
"peergrading/PeerGradingSample": {
"display_name": "Sample Question"
}
}
<course org="edX" course="sa_test" url_name="2012_Fall"/>
<selfassessment attempts='10'> <selfassessment attempts='10'>
<prompt> <prompt>
What is the meaning of life? What is the meaning of life?
</prompt> </prompt>
<rubric> <rubric>
This is a rubric. This is a rubric.
</rubric> </rubric>
<submitmessage> <submitmessage>
Thanks for your submission! Thanks for your submission!
</submitmessage> </submitmessage>
<hintprompt> <hintprompt>
Enter a hint below: Enter a hint below:
</hintprompt> </hintprompt>
</selfassessment> </selfassessment>
\ No newline at end of file
Any place that says "YEAR_SEMESTER" needs to be replaced with something
in the form "2013_Spring". Take note of this name exactly, you'll need to
use it everywhere, precisely - capitalization is very important.
See https://github.com/MITx/mitx/blob/master/doc/xml-format.md for more on all this.
-----------------------
about/: Files that live here will be visible OUTSIDE OF COURSEWARE.
YEAR_SEMESTER/
end_date.html: Specifies in plain-text the end date of the course
overview.html: Text of the overview of the course
short_description.html: 10-15 words about the course
prerequisites.html: Any prerequisites for the course, or None if there are none.
course/
YEAR_SEMESTER.xml: This is your top-level xml page that points at chapters.
Can just be <course/> for now.
course.xml: This top level file points at a file in roots/. See creating_course.xml.
creating_course.xml: Explains how to create course.xml
info/: Files that live here will be visible on the COURSE LANDING PAGE
(Course Info) WITHIN THE COURSEWARE.
YEAR_SEMESTER/
handouts.html: A list of handouts, or an empty file if there are none
(if this file doesn't exist, it displays an error)
updates.html: Course updates.
policies/
YEAR_SEMESTER/
policy.json: See https://github.com/MITx/mitx/blob/master/doc/xml-format.md
for more on the fields specified by this file.
grading_policy.json: Optional -- you don't need it to get a course off the
ground but will eventually. For more info see
https://github.com/MITx/mitx/blob/master/doc/course_grading.md
roots/
YEAR_SEMESTER.xml: Looks something like
<course url_name="YEAR_SEMESTER" org="ORG" course="COURSENUM"/>
where ORG in {"MITx", "HarvardX", "BerkeleyX"}
static/
See README.
images/
course_image.jpg: You MUST have an image named this to be the background
banner image on edx.org
-----------------------
\ No newline at end of file
content-harvard-justicex
========================
\ No newline at end of file
<section class="about">
<h2>About ER22x</h2>
<p>Justice is a critical analysis of classical and contemporary theories of justice, including discussion of present-day applications. Topics include affirmative action, income distribution, same-sex marriage, the role of markets, debates about rights (human rights and property rights), arguments for and against equality, dilemmas of loyalty in public and private life. The course invites students to subject their own views on these controversies to critical examination.</p>
<p>The principle readings for the course are texts by Aristotle, John Locke, Immanuel Kant, John Stuart Mill, and John Rawls. Other assigned readings include writings by contemporary philosophers, court cases, and articles about political controversies that raise philosophical questions.</p>
<!--
<p>The assigned readings will be freely available online. They are also collected in an edited volume, <emph>Justice: A Reader</emph> (ed. Michael Sandel, Oxford University Press). Students who would like further guidance on the themes of the lectures can read Michael Sandel, <emph>Justice: What’s the Right Thing to Do?</emph> (Recommended but not required.)
</p>
-->
</section>
<section class="course-staff">
<h2>Course instructor</h3>
<article class="teacher">
<!-- TODO: Need to change image location -->
<!-- First Professor -->
<div class="teacher-image"><img src="/static/images/professor-sandel.jpg"/></div>
<h3>Michael J. Sandel</h3>
<p>Michael J. Sandel is the Anne T. and Robert M. Bass Professor of Government at Harvard University, where he teaches political philosophy.  His course "Justice" has enrolled more than 15,000 Harvard students.  Sandel's writings have been published in 21 languages.  His books include <i>What Money Can't Buy: The Moral Limits of Markets</i> (2012); <i>Justice: What's the Right Thing to Do?</i> (2009); <i>The Case against Perfection: Ethics in the Age of Genetic Engineering</i> (2007); <i>Public Philosophy: Essays on Morality in Politics</i> (2005); <i>Democracy's Discontent</i> (1996); and <i>Liberalism and the Limits of Justice</i>(1982; 2nd ed., 1998). </p>
<p><br></p>
</section>
<section class="faq">
<section class="responses">
<h2>Frequently Asked Questions</h2>
<article class="response">
<h3>How much does it cost to take the course?</h3>
<p>Nothing! The course is free.</p>
</article>
<article class="response">
<h3>Does the course have any prerequisites?</h3>
<p>No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.</p>
</article>
<article class="response">
<h3>Do I need any other materials to take the course?</h3>
<p>No. As long as you’ve got a computer to access the website, you are ready to take the course.</p>
</article>
<article class="response">
<h3>Is there a textbook for the course?</h3>
<p>All of the course readings that are in the public domain are freely available online, at links provided on the course website. The course can be taken using these free resources alone. For those who wish to purchase a printed version of the assigned readings, an edited volume entitled, Justice: A Reader (ed., Michael Sandel) is available in paperback from Oxford University Press (in bookstores and from online booksellers). Those who would like supplementary readings on the themes of the lectures can find them in Michael Sandel's book Justice: What's the Right Thing to Do?, which is available in various languages throughout the world. This book is not required, and the course can be taken using the free online resources alone.</p>
</article>
<article class="response">
<h3>Do I need to watch the lectures at a specific time?</h3>
<p>No. You can watch the lectures at your leisure.</p>
</article>
<article class="response">
<h3>Will I be able to participate in class discussions?</h3>
<p>Yes, in several ways: </p>
<ol>
<li><p> Each lecture invites you to respond to a poll question related to the themes of the lecture. If you respond to the question, you will be presented with a challenge to the opinion you have expressed, and invited to reply to the challenge. You can also, if you wish, comment on the opinions and responses posted by other students in the course, continuing the discussion.</p></li>
<li><p> In addition to the poll question, each class contains a discussion prompt that invites you to offer your view on a controversial question related to the lecture. If you wish, you can respond to this question, and then see what other students have to say about the argument you present. You can also comment on the opinions posted by other students. One aim of the course is to promote reasoned public dialogue about hard moral and political questions. </p></li>
<li><p> Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.</p></li>
</ol>
</article>
<article class="response">
<h3>Will certificates be awarded?</h3>
<p>Yes. Online learners who achieve a passing grade in a course can earn a certificate of mastery. These certificates will indicate you have successfully completed the course, but will not include a specific grade. Certificates will be issued by edX under the name of HarvardX, designating the institution from which the course originated. </p>
</article>
</section>
</section>
\ No newline at end of file
JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
<iframe width="560" height="315" src="http://www.youtube.com/embed/fajlZMdPkKE#!" frameborder="0" allowfullscreen></iframe>
\ No newline at end of file
<chapter>
<sequential url_name="Problem_Demos"/>
</chapter>
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
<!-- Name this file eg "2012_Fall.xml" or "2013_Spring.xml"
Take note of this name exactly, you'll need to use it everywhere. -->
<course>
<chapter url_name="Staff"/>
</course>
<!-- A file named "course.xml" in your top-level should point
at the appropriate roots file. You can do so like this:
$ rm course.xml
$ ln -s roots/YEAR_SEMESTER.xml course.xml
Ask Sarina for help with this. -->
<ol>
<li>A list of course handouts, or an empty file if there are none.</li>
</ol>
<!-- If you wish to make a welcome announcement -->
<ol>
<li><h2>December 9</h2>
<section class="update-description">
<p>Announcement text</p>
</section>
</li>
</ol>
{
"course/2013_Spring": {
"start": "2099-01-01T00:00",
"advertised_start" : "Spring 2013",
"display_name": "Justice"
}
}
<course url_name="2013_Spring" org="HarvardX" course="ER22x"/>
<sequential>
<vertical name="test_vertical">
<word_cloud name="cloud1" display_name="cloud" num_inputs="5" num_top_words="250" />
</vertical>
</sequential>
Images, handouts, and other statically-served content should go ONLY
in this directory.
Images for the front page should go in static/images. The frontpage
banner MUST be named course_image.jpg
\ No newline at end of file
This diff is collapsed. Click to expand it.
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