Commit 18c91d0f by Calen Pennington

Merge remote-tracking branch 'edx/master' into opaque-keys-merge-master

Conflicts:
	cms/djangoapps/contentstore/tests/utils.py
	cms/djangoapps/contentstore/views/import_export.py
	cms/djangoapps/contentstore/views/tests/test_import_export.py
	common/djangoapps/student/views.py
	lms/djangoapps/class_dashboard/dashboard_data.py
	lms/djangoapps/instructor/views/instructor_dashboard.py
	lms/static/js/staff_debug_actions.js
	lms/templates/notes.html
	lms/templates/staff_problem_info.html
parents af8cab2a 6396373b
...@@ -38,13 +38,14 @@ Feature: CMS.Create Subsection ...@@ -38,13 +38,14 @@ Feature: CMS.Create Subsection
Then I see the subsection release date is 12/25/2011 03:00 Then I see the subsection release date is 12/25/2011 03:00
And I see the subsection due date is 01/02/2012 04:00 And I see the subsection due date is 01/02/2012 04:00
Scenario: Set release and due dates of subsection on enter # Disabling due to failure on master. JZ 05/14/2014 TODO: fix
Given I have opened a new subsection in Studio # Scenario: Set release and due dates of subsection on enter
And I set the subsection release date on enter to 04/04/2014 03:00 # Given I have opened a new subsection in Studio
And I set the subsection due date on enter to 04/04/2014 04:00 # And I set the subsection release date on enter to 04/04/2014 03:00
And I reload the page # And I set the subsection due date on enter to 04/04/2014 04:00
Then I see the subsection release date is 04/04/2014 03:00 # And I reload the page
And I see the subsection due date is 04/04/2014 04:00 # Then I see the subsection release date is 04/04/2014 03:00
# And I see the subsection due date is 04/04/2014 04:00
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
...@@ -55,15 +56,16 @@ Feature: CMS.Create Subsection ...@@ -55,15 +56,16 @@ Feature: CMS.Create Subsection
And I confirm the prompt And I confirm the prompt
Then the subsection does not exist Then the subsection does not exist
Scenario: Sync to Section # Disabling due to failure on master. JZ 05/14/2014 TODO: fix
Given I have opened a new course section in Studio # Scenario: Sync to Section
And I click the Edit link for the release date # Given I have opened a new course section in Studio
And I set the section release date to 01/02/2103 # And I click the Edit link for the release date
And I have added a new subsection # And I set the section release date to 01/02/2103
And I click on the subsection # And I have added a new subsection
And I set the subsection release date to 01/20/2103 # And I click on the subsection
And I reload the page # And I set the subsection release date to 01/20/2103
And I click the link to sync release date to section # And I reload the page
And I wait for "1" second # And I click the link to sync release date to section
And I reload the page # And I wait for "1" second
Then I see the subsection release date is 01/02/2103 # And I reload the page
# Then I see the subsection release date is 01/02/2103
...@@ -4,15 +4,19 @@ Utilities for contentstore tests ...@@ -4,15 +4,19 @@ Utilities for contentstore tests
import json import json
from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
<<<<<<< HEAD
=======
from student.models import Registration
>>>>>>> edx/master
def parse_json(response): def parse_json(response):
...@@ -93,9 +97,9 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -93,9 +97,9 @@ class CourseTestCase(ModuleStoreTestCase):
) )
self.store = get_modulestore(self.course.location) self.store = get_modulestore(self.course.location)
def create_non_staff_authed_user_client(self): def create_non_staff_authed_user_client(self, authenticate=True):
""" """
Create a non-staff user, log them in, and return the client, user to use for testing. Create a non-staff user, log them in (if authenticate=True), and return the client, user to use for testing.
""" """
uname = 'teststudent' uname = 'teststudent'
password = 'foo' password = 'foo'
...@@ -108,7 +112,8 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -108,7 +112,8 @@ class CourseTestCase(ModuleStoreTestCase):
nonstaff.save() nonstaff.save()
client = Client() client = Client()
client.login(username=uname, password=password) if authenticate:
client.login(username=uname, password=password)
return client, nonstaff return client, nonstaff
def populate_course(self): def populate_course(self):
......
...@@ -4,38 +4,44 @@ courses ...@@ -4,38 +4,44 @@ courses
""" """
import logging import logging
import os import os
import tarfile
import shutil
import re import re
from tempfile import mkdtemp import shutil
import tarfile
from path import path from path import path
from tempfile import mkdtemp
from django.conf import settings from django.conf import settings
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.core.servers.basehttp import FileWrapper
from django.core.files.temp import NamedTemporaryFile
from django.core.exceptions import SuspiciousOperation, PermissionDenied from django.core.exceptions import SuspiciousOperation, PermissionDenied
from django.http import HttpResponseNotFound from django.core.files.temp import NamedTemporaryFile
from django.views.decorators.http import require_http_methods, require_GET from django.core.servers.basehttp import FileWrapper
from django.http import HttpResponse, HttpResponseNotFound
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods, require_GET
from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
<<<<<<< HEAD
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.keys import CourseKey from xmodule.modulestore.keys import CourseKey
from xmodule.exceptions import SerializationError from xmodule.exceptions import SerializationError
from .access import has_course_access from .access import has_course_access
=======
from xmodule.exceptions import SerializationError
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_exporter import export_to_xml
>>>>>>> edx/master
from util.json_request import JsonResponse from .access import has_course_access
from extract_tar import safetar_extractall from extract_tar import safetar_extractall
from student.roles import CourseInstructorRole, CourseStaffRole
from student import auth from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
from util.json_request import JsonResponse
from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.utils import reverse_course_url, reverse_usage_url
...@@ -234,10 +240,13 @@ def import_handler(request, course_key_string): ...@@ -234,10 +240,13 @@ def import_handler(request, course_key_string):
session_status[key] = 3 session_status[key] = 3
request.session.modified = True request.session.modified = True
<<<<<<< HEAD
auth.add_users(request.user, CourseInstructorRole(new_location.course_key), request.user) auth.add_users(request.user, CourseInstructorRole(new_location.course_key), request.user)
auth.add_users(request.user, CourseStaffRole(new_location.course_key), request.user) auth.add_users(request.user, CourseStaffRole(new_location.course_key), request.user)
logging.debug('created all course groups at {0}'.format(new_location)) logging.debug('created all course groups at {0}'.format(new_location))
=======
>>>>>>> edx/master
# Send errors to client with stage at which error occurred. # Send errors to client with stage at which error occurred.
except Exception as exception: # pylint: disable=W0703 except Exception as exception: # pylint: disable=W0703
log.exception( log.exception(
......
""" """
Unit tests for course import and export Unit tests for course import and export
""" """
import copy
import json
import logging
import os import os
import shutil import shutil
import tarfile import tarfile
import tempfile import tempfile
import copy
from path import path from path import path
import json
import logging
from uuid import uuid4
from pymongo import MongoClient from pymongo import MongoClient
from uuid import uuid4
from contentstore.tests.utils import CourseTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
<<<<<<< HEAD
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
=======
>>>>>>> edx/master
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.tests.factories import ItemFactory from xmodule.modulestore.tests.factories import ItemFactory
from contentstore.tests.utils import CourseTestCase
from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
...@@ -105,6 +112,46 @@ class ImportTestCase(CourseTestCase): ...@@ -105,6 +112,46 @@ class ImportTestCase(CourseTestCase):
self.assertEquals(resp.status_code, 200) self.assertEquals(resp.status_code, 200)
def test_import_in_existing_course(self):
"""
Check that course is imported successfully in existing course and users have their access roles
"""
# Create a non_staff user and add it to course staff only
__, nonstaff_user = self.create_non_staff_authed_user_client(authenticate=False)
auth.add_users(self.user, CourseStaffRole(self.course.location), nonstaff_user)
course = self.store.get_item(self.course_location)
self.assertIsNotNone(course)
display_name_before_import = course.display_name
# Check that global staff user can import course
with open(self.good_tar) as gtar:
args = {"name": self.good_tar, "course-data": [gtar]}
resp = self.client.post(self.url, args)
self.assertEquals(resp.status_code, 200)
course = self.store.get_item(self.course_location)
self.assertIsNotNone(course)
display_name_after_import = course.display_name
# Check that course display name have changed after import
self.assertNotEqual(display_name_before_import, display_name_after_import)
# Now check that non_staff user has his same role
self.assertFalse(CourseInstructorRole(self.course_location).has_user(nonstaff_user))
self.assertTrue(CourseStaffRole(self.course_location).has_user(nonstaff_user))
# Now course staff user can also successfully import course
self.client.login(username=nonstaff_user.username, password='foo')
with open(self.good_tar) as gtar:
args = {"name": self.good_tar, "course-data": [gtar]}
resp = self.client.post(self.url, args)
self.assertEquals(resp.status_code, 200)
# Now check that non_staff user has his same role
self.assertFalse(CourseInstructorRole(self.course_location).has_user(nonstaff_user))
self.assertTrue(CourseStaffRole(self.course_location).has_user(nonstaff_user))
## Unsafe tar methods ##################################################### ## Unsafe tar methods #####################################################
# Each of these methods creates a tarfile with a single type of unsafe # Each of these methods creates a tarfile with a single type of unsafe
# content. # content.
......
...@@ -318,7 +318,7 @@ PIPELINE_CSS = { ...@@ -318,7 +318,7 @@ PIPELINE_CSS = {
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css', 'css/vendor/jquery.qtip.min.css',
'js/vendor/markitup/skins/simple/style.css', 'js/vendor/markitup/skins/simple/style.css',
'js/vendor/markitup/sets/wiki/style.css', 'js/vendor/markitup/sets/wiki/style.css'
], ],
'output_filename': 'css/cms-style-vendor.css', 'output_filename': 'css/cms-style-vendor.css',
}, },
......
'''
Firebase - library to generate a token
License: https://github.com/firebase/firebase-token-generator-python/blob/master/LICENSE
Tweaked and Edited by @danielcebrianr and @lduarte1991
This library will take either objects or strings and use python's built-in encoding
system as specified by RFC 3548. Thanks to the firebase team for their open-source
library. This was made specifically for speaking with the annotation_storage_url and
can be used and expanded, but not modified by anyone else needing such a process.
'''
from base64 import urlsafe_b64encode
import hashlib
import hmac
import sys
try:
import json
except ImportError:
import simplejson as json
__all__ = ['create_token']
TOKEN_SEP = '.'
def create_token(secret, data):
'''
Simply takes in the secret key and the data and
passes it to the local function _encode_token
'''
return _encode_token(secret, data)
if sys.version_info < (2, 7):
def _encode(bytes_data):
'''
Takes a json object, string, or binary and
uses python's urlsafe_b64encode to encode data
and make it safe pass along in a url.
To make sure it does not conflict with variables
we make sure equal signs are removed.
More info: docs.python.org/2/library/base64.html
'''
encoded = urlsafe_b64encode(bytes(bytes_data))
return encoded.decode('utf-8').replace('=', '')
else:
def _encode(bytes_info):
'''
Same as above function but for Python 2.7 or later
'''
encoded = urlsafe_b64encode(bytes_info)
return encoded.decode('utf-8').replace('=', '')
def _encode_json(obj):
'''
Before a python dict object can be properly encoded,
it must be transformed into a jason object and then
transformed into bytes to be encoded using the function
defined above.
'''
return _encode(bytearray(json.dumps(obj), 'utf-8'))
def _sign(secret, to_sign):
'''
This function creates a sign that goes at the end of the
message that is specific to the secret and not the actual
content of the encoded body.
More info on hashing: http://docs.python.org/2/library/hmac.html
The function creates a hashed values of the secret and to_sign
and returns the digested values based the secure hash
algorithm, 256
'''
def portable_bytes(string):
'''
Simply transforms a string into a bytes object,
which is a series of immutable integers 0<=x<=256.
Always try to encode as utf-8, unless it is not
compliant.
'''
try:
return bytes(string, 'utf-8')
except TypeError:
return bytes(string)
return _encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), hashlib.sha256).digest()) # pylint: disable=E1101
def _encode_token(secret, claims):
'''
This is the main function that takes the secret token and
the data to be transmitted. There is a header created for decoding
purposes. Token_SEP means that a period/full stop separates the
header, data object/message, and signatures.
'''
encoded_header = _encode_json({'typ': 'JWT', 'alg': 'HS256'})
encoded_claims = _encode_json(claims)
secure_bits = '%s%s%s' % (encoded_header, TOKEN_SEP, encoded_claims)
sig = _sign(secret, secure_bits)
return '%s%s%s' % (secure_bits, TOKEN_SEP, sig)
"""
This test will run for firebase_token_generator.py.
"""
from django.test import TestCase
from student.firebase_token_generator import _encode, _encode_json, _encode_token, create_token
class TokenGenerator(TestCase):
"""
Tests for the file firebase_token_generator.py
"""
def test_encode(self):
"""
This tests makes sure that no matter what version of python
you have, the _encode function still returns the appropriate result
for a string.
"""
expected = "dGVzdDE"
result = _encode("test1")
self.assertEqual(expected, result)
def test_encode_json(self):
"""
Same as above, but this one focuses on a python dict type
transformed into a json object and then encoded.
"""
expected = "eyJ0d28iOiAidGVzdDIiLCAib25lIjogInRlc3QxIn0"
result = _encode_json({'one': 'test1', 'two': 'test2'})
self.assertEqual(expected, result)
def test_create_token(self):
"""
Unlike its counterpart in student/views.py, this function
just checks for the encoding of a token. The other function
will test depending on time and user.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.-p1sr7uwCapidTQ0qB7DdU2dbF-hViKpPNN_5vD10t8"
result1 = _encode_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
result2 = create_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
self.assertEqual(expected, result1)
self.assertEqual(expected, result2)
...@@ -27,7 +27,7 @@ from mock import Mock, patch ...@@ -27,7 +27,7 @@ from mock import Mock, patch
from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user from student.models import anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user
from student.views import (process_survey_link, _cert_info, from student.views import (process_survey_link, _cert_info,
change_enrollment, complete_course_mode_info) change_enrollment, complete_course_mode_info, token)
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
import shoppingcart import shoppingcart
...@@ -491,3 +491,26 @@ class AnonymousLookupTable(TestCase): ...@@ -491,3 +491,26 @@ class AnonymousLookupTable(TestCase):
anonymous_id = anonymous_id_for_user(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id)
real_user = user_by_anonymous_id(anonymous_id) real_user = user_by_anonymous_id(anonymous_id)
self.assertEqual(self.user, real_user) self.assertEqual(self.user, real_user)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class Token(ModuleStoreTestCase):
"""
Test for the token generator. This creates a random course and passes it through the token file which generates the
token that will be passed in to the annotation_storage_url.
"""
request_factory = RequestFactory()
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "edx"
def setUp(self):
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.user = User.objects.create(username="username", email="username")
self.req = self.request_factory.post('/token?course_id=edx/100/test_course', {'user': self.user})
self.req.user = self.user
def test_token(self):
expected = HttpResponse("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAxLTIzVDE5OjM1OjE3LjUyMjEwNC01OjAwIiwgImNvbnN1bWVyS2V5IjogInh4eHh4eHh4LXh4eHgteHh4eC14eHh4LXh4eHh4eHh4eHh4eCIsICJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.OjWz9mzqJnYuzX-f3uCBllqJUa8PVWJjcDy_McfxLvc", mimetype="text/plain")
response = token(self.req)
self.assertEqual(expected.content.split('.')[0], response.content.split('.')[0])
...@@ -44,6 +44,7 @@ from student.models import ( ...@@ -44,6 +44,7 @@ from student.models import (
create_comments_service_user, PasswordHistory create_comments_service_user, PasswordHistory
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from student.firebase_token_generator import create_token
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
...@@ -1867,7 +1868,11 @@ def token(request): ...@@ -1867,7 +1868,11 @@ def token(request):
the token was issued. This will be stored with the user along with the token was issued. This will be stored with the user along with
the id for identification purposes in the backend. the id for identification purposes in the backend.
''' '''
<<<<<<< HEAD
course_id = SlashSeparatedCourseKey.from_deprecated_string(request.GET.get("course_id")) course_id = SlashSeparatedCourseKey.from_deprecated_string(request.GET.get("course_id"))
=======
course_id = request.GET.get("course_id")
>>>>>>> edx/master
course = course_from_id(course_id) course = course_from_id(course_id)
dtnow = datetime.datetime.now() dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow() dtutcnow = datetime.datetime.utcnow()
......
"""
This file contains a function used to retrieve the token for the annotation backend
without having to create a view, but just returning a string instead.
It can be called from other files by using the following:
from xmodule.annotator_token import retrieve_token
"""
import datetime
from firebase_token_generator import create_token
def retrieve_token(userid, secret):
'''
Return a token for the backend of annotations.
It uses the course id to retrieve a variable that contains the secret
token found in inheritance.py. It also contains information of when
the token was issued. This will be stored with the user along with
the id for identification purposes in the backend.
'''
# the following five lines of code allows you to include the default timezone in the iso format
# for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone
dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow()
delta = dtnow - dtutcnow
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
# uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a
# federated system in the annotation backend server
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
newtoken = create_token(secret, custom_data)
return newtoken
"""
This test will run for annotator_token.py
"""
import unittest
from xmodule.annotator_token import retrieve_token
class TokenRetriever(unittest.TestCase):
"""
Tests to make sure that when passed in a username and secret token, that it will be encoded correctly
"""
def test_token(self):
"""
Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
response = retrieve_token("username", "fake_secret")
self.assertEqual(expected.split('.')[0], response.split('.')[0])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
\ No newline at end of file
...@@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase): ...@@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
ScopeIds(None, None, None, None) ScopeIds(None, None, None, None)
) )
def test_render_content(self):
"""
Tests to make sure the sample xml is rendered and that it forms a valid xmltree
that does not contain a display_name.
"""
content = self.mod._render_content() # pylint: disable=W0212
self.assertIsNotNone(content)
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self): def test_extract_instructions(self):
""" """
Tests to make sure that the instructions are correctly pulled from the sample xml above. Tests to make sure that the instructions are correctly pulled from the sample xml above.
...@@ -59,5 +70,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase): ...@@ -59,5 +70,5 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
Tests the function that passes in all the information in the context that will be used in templates/textannotation.html Tests the function that passes in all the information in the context that will be used in templates/textannotation.html
""" """
context = self.mod.get_html() context = self.mod.get_html()
for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage', 'token']: for key in ['display_name', 'tag', 'source', 'instructions_html', 'content_html', 'annotation_storage']:
self.assertIn(key, context) self.assertIn(key, context)
...@@ -34,6 +34,100 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): ...@@ -34,6 +34,100 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
ScopeIds(None, None, None, None) ScopeIds(None, None, None, None)
) )
def test_annotation_class_attr_default(self):
"""
Makes sure that it can detect annotation values in text-form if user
decides to add text to the area below video, video functionality is completely
found in javascript.
"""
xml = '<annotation title="x" body="y" problem="0">test</annotation>'
element = etree.fromstring(xml)
expected_attr = {'class': {'value': 'annotatable-span highlight'}}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_valid_highlight(self):
"""
Same as above but more specific to an area that is highlightable in the appropriate
color designated.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for color in self.mod.highlight_colors:
element = etree.fromstring(xml.format(highlight=color))
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
expected_attr = {'class': {
'value': value,
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_invalid_highlight(self):
"""
Same as above, but checked with invalid colors.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
element = etree.fromstring(xml.format(highlight=invalid_color))
expected_attr = {'class': {
'value': 'annotatable-span highlight',
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_data_attr(self):
"""
Test that each highlight contains the data information from the annotation itself.
"""
element = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
expected_attr = {
'data-comment-body': {'value': 'foo', '_delete': 'body'},
'data-comment-title': {'value': 'bar', '_delete': 'title'},
'data-problem-id': {'value': '0', '_delete': 'problem'}
}
actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_render_annotation(self):
"""
Tests to make sure that the spans designating annotations acutally visually render as annotations.
"""
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
self.mod._render_annotation(actual_el) # pylint: disable=W0212
self.assertEqual(expected_el.tag, actual_el.tag)
self.assertEqual(expected_el.text, actual_el.text)
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
def test_render_content(self):
"""
Like above, but using the entire text, it makes sure that display_name is removed and that there is only one
div encompassing the annotatable area.
"""
content = self.mod._render_content() # pylint: disable=W0212
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertEqual('div', element.tag, 'root tag is a div')
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self): def test_extract_instructions(self):
""" """
This test ensures that if an instruction exists it is pulled and This test ensures that if an instruction exists it is pulled and
...@@ -66,6 +160,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): ...@@ -66,6 +160,6 @@ class VideoAnnotationModuleTestCase(unittest.TestCase):
""" """
Tests to make sure variables passed in truly exist within the html once it is all rendered. Tests to make sure variables passed in truly exist within the html once it is all rendered.
""" """
context = self.mod.get_html() # pylint: disable=W0212 context = self.mod.get_html()
for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']: for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']:
self.assertIn(key, context) self.assertIn(key, context)
...@@ -6,7 +6,6 @@ from pkg_resources import resource_string ...@@ -6,7 +6,6 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String from xblock.core import Scope, String
from xmodule.annotator_token import retrieve_token
import textwrap import textwrap
...@@ -31,7 +30,7 @@ class AnnotatableFields(object): ...@@ -31,7 +30,7 @@ class AnnotatableFields(object):
scope=Scope.settings, scope=Scope.settings,
default='Text Annotation', default='Text Annotation',
) )
instructor_tags = String( tags = String(
display_name="Tags for Assignments", display_name="Tags for Assignments",
help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue", help="Add tags that automatically highlight in a certain color using the comma-separated form, i.e. imagery:red,parallelism:blue",
scope=Scope.settings, scope=Scope.settings,
...@@ -44,7 +43,6 @@ class AnnotatableFields(object): ...@@ -44,7 +43,6 @@ class AnnotatableFields(object):
default='None', default='None',
) )
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage") annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
class TextAnnotationModule(AnnotatableFields, XModule): class TextAnnotationModule(AnnotatableFields, XModule):
...@@ -61,9 +59,15 @@ class TextAnnotationModule(AnnotatableFields, XModule): ...@@ -61,9 +59,15 @@ class TextAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree) self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode') self.content = etree.tostring(xmltree, encoding='unicode')
self.user_email = "" self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
if self.runtime.get_real_user is not None:
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
return etree.tostring(xmltree, encoding='unicode')
def _extract_instructions(self, xmltree): def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """ """ Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
...@@ -78,13 +82,13 @@ class TextAnnotationModule(AnnotatableFields, XModule): ...@@ -78,13 +82,13 @@ class TextAnnotationModule(AnnotatableFields, XModule):
""" Renders parameters to template. """ """ Renders parameters to template. """
context = { context = {
'display_name': self.display_name_with_default, 'display_name': self.display_name_with_default,
'tag': self.instructor_tags, 'tag': self.tags,
'source': self.source, 'source': self.source,
'instructions_html': self.instructions, 'instructions_html': self.instructions,
'content_html': self.content, 'content_html': self._render_content(),
'annotation_storage': self.annotation_storage_url, 'annotation_storage': self.annotation_storage_url
'token': retrieve_token(self.user_email, self.annotation_token_secret),
} }
return self.system.render_template('textannotation.html', context) return self.system.render_template('textannotation.html', context)
...@@ -97,7 +101,6 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor): ...@@ -97,7 +101,6 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([ non_editable_fields.extend([
TextAnnotationDescriptor.annotation_storage_url, TextAnnotationDescriptor.annotation_storage_url
TextAnnotationDescriptor.annotation_token_secret,
]) ])
return non_editable_fields return non_editable_fields
...@@ -7,7 +7,6 @@ from pkg_resources import resource_string ...@@ -7,7 +7,6 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String from xblock.core import Scope, String
from xmodule.annotator_token import retrieve_token
import textwrap import textwrap
...@@ -32,7 +31,7 @@ class AnnotatableFields(object): ...@@ -32,7 +31,7 @@ class AnnotatableFields(object):
sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4") sourceurl = String(help="The external source URL for the video.", display_name="Source URL", scope=Scope.settings, default="http://video-js.zencoder.com/oceans-clip.mp4")
poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="") poster_url = String(help="Poster Image URL", display_name="Poster URL", scope=Scope.settings, default="")
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage") annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
class VideoAnnotationModule(AnnotatableFields, XModule): class VideoAnnotationModule(AnnotatableFields, XModule):
'''Video Annotation Module''' '''Video Annotation Module'''
...@@ -56,9 +55,73 @@ class VideoAnnotationModule(AnnotatableFields, XModule): ...@@ -56,9 +55,73 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
self.instructions = self._extract_instructions(xmltree) self.instructions = self._extract_instructions(xmltree)
self.content = etree.tostring(xmltree, encoding='unicode') self.content = etree.tostring(xmltree, encoding='unicode')
self.user_email = "" self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green']
if self.runtime.get_real_user is not None:
self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email def _get_annotation_class_attr(self, element):
""" Returns a dict with the CSS class attribute to set on the annotation
and an XML key to delete from the element.
"""
attr = {}
cls = ['annotatable-span', 'highlight']
highlight_key = 'highlight'
color = element.get(highlight_key)
if color is not None:
if color in self.highlight_colors:
cls.append('highlight-' + color)
attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls)
return {'class': attr}
def _get_annotation_data_attr(self, element):
""" Returns a dict in which the keys are the HTML data attributes
to set on the annotation element. Each data attribute has a
corresponding 'value' and (optional) '_delete' key to specify
an XML attribute to delete.
"""
data_attrs = {}
attrs_map = {
'body': 'data-comment-body',
'title': 'data-comment-title',
'problem': 'data-problem-id'
}
for xml_key in attrs_map.keys():
if xml_key in element.attrib:
value = element.get(xml_key, '')
html_key = attrs_map[xml_key]
data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs
def _render_annotation(self, element):
""" Renders an annotation element for HTML output. """
attr = {}
attr.update(self._get_annotation_class_attr(element))
attr.update(self._get_annotation_data_attr(element))
element.tag = 'span'
for key in attr.keys():
element.set(key, attr[key]['value'])
if '_delete' in attr[key] and attr[key]['_delete'] is not None:
delete_key = attr[key]['_delete']
del element.attrib[delete_key]
def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content)
xmltree.tag = 'div'
if 'display_name' in xmltree.attrib:
del xmltree.attrib['display_name']
for element in xmltree.findall('.//annotation'):
self._render_annotation(element)
return etree.tostring(xmltree, encoding='unicode')
def _extract_instructions(self, xmltree): def _extract_instructions(self, xmltree):
""" Removes <instructions> from the xmltree and returns them as a string, otherwise None. """ """ Removes <instructions> from the xmltree and returns them as a string, otherwise None. """
...@@ -91,9 +154,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule): ...@@ -91,9 +154,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
'sourceUrl': self.sourceurl, 'sourceUrl': self.sourceurl,
'typeSource': extension, 'typeSource': extension,
'poster': self.poster_url, 'poster': self.poster_url,
'content_html': self.content, 'alert': self,
'annotation_storage': self.annotation_storage_url, 'content_html': self._render_content(),
'token': retrieve_token(self.user_email, self.annotation_token_secret), 'annotation_storage': self.annotation_storage_url
} }
return self.system.render_template('videoannotation.html', context) return self.system.render_template('videoannotation.html', context)
...@@ -108,7 +171,6 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor): ...@@ -108,7 +171,6 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor):
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([ non_editable_fields.extend([
VideoAnnotationDescriptor.annotation_storage_url, VideoAnnotationDescriptor.annotation_storage_url
VideoAnnotationDescriptor.annotation_token_secret,
]) ])
return non_editable_fields return non_editable_fields
Annotator.Plugin.Auth.prototype.haveValidToken = function() {
return (
this._unsafeToken &&
this._unsafeToken.d.issuedAt &&
this._unsafeToken.d.ttl &&
this._unsafeToken.d.consumerKey &&
this.timeToExpiry() > 0
);
};
Annotator.Plugin.Auth.prototype.timeToExpiry = function() {
var expiry, issue, now, timeToExpiry;
now = new Date().getTime() / 1000;
issue = createDateFromISO8601(this._unsafeToken.d.issuedAt).getTime() / 1000;
expiry = issue + this._unsafeToken.d.ttl;
timeToExpiry = expiry - now;
if (timeToExpiry > 0) {
return timeToExpiry;
} else {
return 0;
}
};
\ No newline at end of file
...@@ -64,7 +64,7 @@ class RegistrationTest(UniqueCourseTest): ...@@ -64,7 +64,7 @@ class RegistrationTest(UniqueCourseTest):
course_names = dashboard.available_courses course_names = dashboard.available_courses
self.assertIn(self.course_info['display_name'], course_names) self.assertIn(self.course_info['display_name'], course_names)
@skip("TE-399")
class LanguageTest(UniqueCourseTest): class LanguageTest(UniqueCourseTest):
""" """
Tests that the change language functionality on the dashboard works Tests that the change language functionality on the dashboard works
...@@ -381,6 +381,10 @@ class XBlockAcidNoChildTest(XBlockAcidBase): ...@@ -381,6 +381,10 @@ class XBlockAcidNoChildTest(XBlockAcidBase):
) )
).install() ).install()
@skip('Flakey test, TE-401')
def test_acid_block(self):
super(XBlockAcidNoChildTest, self).test_acid_block()
class XBlockAcidChildTest(XBlockAcidBase): class XBlockAcidChildTest(XBlockAcidBase):
""" """
......
...@@ -12,10 +12,12 @@ May, 2014 ...@@ -12,10 +12,12 @@ May, 2014
* - Date * - Date
- Change - Change
* - 05/16/14
- Updated :ref:`Working with Video Components` to reflect UI changes.
* - 05/14/14 * - 05/14/14
- Updated the :ref:`Running Your Course Index` chapter to remove references - Updated the :ref:`Running Your Course Index` chapter to remove references
to the "new beta" Instructor Dashboard. to the "new beta" Instructor Dashboard.
* - * - 05/13/14
- Updated the :ref:`Enrollment` section to reflect that usernames or email - Updated the :ref:`Enrollment` section to reflect that usernames or email
addresses can be used to batch enroll students. addresses can be used to batch enroll students.
* - * -
......
...@@ -113,16 +113,15 @@ company that provides captioning services. EdX works with `3Play Media ...@@ -113,16 +113,15 @@ company that provides captioning services. EdX works with `3Play Media
<http://www.3playmedia.com>`_. `YouTube <http://www.youtube.com/>`_ also <http://www.3playmedia.com>`_. `YouTube <http://www.youtube.com/>`_ also
provides captioning services. provides captioning services.
In addition to your .srt file, you can provide other transcripts with your When you upload an .srt file, a .txt file is created automatically. You can allow students to download either the .srt file or the .txt file. You can also provide transcripts in different formats, such as .pdf, and you can provide transcripts in different languages. For more information, see :ref:`Additional Transcripts`.
video. For example, you can provide downloadable transcripts in a text format
such as .txt or .pdf, and you can provide transcripts in different languages.
For more information, see :ref:`Additional Transcripts`.
If you provide transcripts for students to download, a **Download transcript** If you allow your students to download transcripts, a **Download transcript**
button appears under the video. Students can then select either **SubRip (.srt) button appears under the video. Students can then select either **SubRip (.srt)
file** or **Text (.txt) file** to download the .srt or .txt transcript. file** or **Text (.txt) file** to download the .srt or .txt transcript.
.. image:: ../Images/transcript-download.png .. image:: /Images/Video_DownTrans_srt-txt.png
:width: 500
:alt: Video status bar showing srt and txt transcript download options
.. note:: Some past courses have used .sjson files for video transcripts. If .. note:: Some past courses have used .sjson files for video transcripts. If
transcripts in your course uses this format, see :ref:`Steps for sjson transcripts in your course uses this format, see :ref:`Steps for sjson
...@@ -141,8 +140,8 @@ Because YouTube is not available in all locations, however, we recommend that ...@@ -141,8 +140,8 @@ Because YouTube is not available in all locations, however, we recommend that
you also post copies of your videos on a third-party site such as `Amazon S3 you also post copies of your videos on a third-party site such as `Amazon S3
<http://aws.amazon.com/s3/>`_. When a student views a video in your course, if <http://aws.amazon.com/s3/>`_. When a student views a video in your course, if
YouTube is not available in that student’s location or if the YouTube video YouTube is not available in that student’s location or if the YouTube video
doesn’t play, the video on the backup site starts playing automatically. The doesn’t play, the video on the backup site starts playing automatically. You can also allow the
student can also click a link to download the video from the backup site. student to download the video from the backup site.
After you post your video online, make sure you have the URL for the video. If After you post your video online, make sure you have the URL for the video. If
you host copies of your video in more than one place, make sure you have the URL you host copies of your video in more than one place, make sure you have the URL
...@@ -171,8 +170,6 @@ site where you post the videos may have to handle a lot of traffic. ...@@ -171,8 +170,6 @@ site where you post the videos may have to handle a lot of traffic.
.mp4, .mpeg, .ogg, or .webm. EdX can't support videos that you post on sites .mp4, .mpeg, .ogg, or .webm. EdX can't support videos that you post on sites
such as Vimeo. such as Vimeo.
.. _Create a Video Component: .. _Create a Video Component:
******************************** ********************************
...@@ -186,42 +183,44 @@ Step 4. Create a Video Component ...@@ -186,42 +183,44 @@ Step 4. Create a Video Component
.. image:: ../Images/VideoComponentEditor.png .. image:: ../Images/VideoComponentEditor.png
:alt: Image of the video component editor :alt: Image of the video component editor
:width: 500
You'll replace the default values with your own. You'll replace the default values with your own.
#. In the **Display Name** field, enter the name you want students to see when #. In the **Component Display Name** field, enter the name you want students to see when
they hover the mouse over the unit in the course ribbon. This text also they hover the mouse over the unit in the course ribbon. This text also
appears as a header for the video. appears as a header for the video.
#. In the **Video URL** field, enter the URL of the video. For example, the URL #. In the **Default Video URL** field, enter the URL of the video. For example, the URL
may resemble one of the following. may resemble one of the following.
:: ::
http://youtu.be/OEoXaMPEzfM http://youtu.be/OEoXaMPEzfM
http://www.youtube.com/watch?v=OEoXaMPEzfM http://www.youtube.com/watch?v=OEoXaMPEzfM
https://s3.amazonaws.com/edx-course-videos/edx-edx101/EDXSPCPJSP13-G030300.mp4 https://s3.amazonaws.com/edx-course-videos/edx-edx101/EDXSPCPJSP13-G030300.mp4
https://s3.amazonaws.com/edx-videos/edx101/video4.webm
.. note:: To be sure all students can access the video, we recommend providing both an .mp4 and a .webm version of your video. To do this, you can post additional versions of your videos on the Internet, then add the URLs for these versions below the default video URL. **These URLs cannot be YouTube URLs**. To add a URL for another version, click **Add URLs for additional versions**. The first listed video that's compatible with the student's computer will play.
#. Next to **Timed Transcript**, select an option. #. Next to **Default Timed Transcript**, select an option.
- If edX already has a transcript for your video--for example, if you're - If edX already has a transcript for your video--for example, if you're
using a video from an existing course--Studio automatically finds the using a video from an existing course--Studio automatically finds the
transcript and associates the transcript with the video. transcript and associates the transcript with the video.
If you want to modify the transcript, click **Download to Edit**. You can If you want to modify the transcript, click **Download Transcript for Editing**. You can then make your changes and upload the new file by clicking **Upload New Transcript**.
then make your changes and upload the new file by clicking **Upload New
Timed Transcript**.
- If your video has a transcript on YouTube, Studio automatically finds the - If edX doesn't have a transcript for the video, but YouTube has a transcript, Studio automatically finds the YouTube
transcript and asks if you want to import it. To use this YouTube transcript and asks if you want to import it. To use this YouTube
transcript, click **Import from YouTube**. (If you want to modify the transcript, click **Import YouTube Transcript**. (If you want to modify the
YouTube transcript, after Studio imports the transcript, click **Download YouTube transcript, import the YouTube transcript into Studio, and then click **Download Transcript for Editing**. You can then make your changes and upload the new file by
to Edit**. You can then make your changes and upload the new file by clicking **Upload New Transcript**.)
clicking **Upload New Timed Transcript**.)
- If both edX and YouTube have a transcript for your video, but the edX transcript is out of date, you'll receive a message asking if you want to replace the edX transcript with the YouTube transcript. To use the YouTube transcript, click **Yes, replace the edX transcript with the YouTube transcript**.
- If neither edX nor YouTube has a transcript for your video, and your - If neither edX nor YouTube has a transcript for your video, and your
transcript uses the .srt format, click **Upload New Timed Transcript** to transcript uses the .srt format, click **Upload New Transcript** to
upload the transcript file from your computer. upload the transcript file from your computer.
.. note:: .. note::
...@@ -229,13 +228,13 @@ Step 4. Create a Video Component ...@@ -229,13 +228,13 @@ Step 4. Create a Video Component
* If your transcript uses the .sjson format, do not use this setting. * If your transcript uses the .sjson format, do not use this setting.
For more information, see :ref:`Steps for sjson files`. For more information, see :ref:`Steps for sjson files`.
* If you want to provide a transcript in a format such as .txt or .pdf, * If you want to provide a transcript in a format such as .pdf,
do not use this setting to upload the transcript. For more do not use this setting to upload the transcript. For more
information, see :ref:`Additional Transcripts`. information, see :ref:`Additional Transcripts`.
#. Optionally, click **Advanced** to set more options for the video. For a #. Optionally, click **Advanced** to set more options for the video. For a
description of each option, see the list below. description of each option, see :ref:`Video Advanced Options`.
#. Click **Save.** #. Click **Save.**
...@@ -247,78 +246,75 @@ Advanced Options ...@@ -247,78 +246,75 @@ Advanced Options
The following options appear on the **Advanced** tab in the Video component. The following options appear on the **Advanced** tab in the Video component.
* **Display Name**: The name that you want your students to see. This is the .. list-table::
same as the **Display Name** field on the **Basic** tab. :widths: 30 70
* **Download Transcript**: The URL for the transcript file for the video. This * - **Component Display Name**
file is usually an .srt file, but can also be a .txt or .pdf file. (For more - The name that you want your students to see. This is the same as the **Display Name** field on the **Basic** tab.
information about .txt and .pdf files, see :ref:`Additional Transcripts`.) The * - **Default Timed Transcript**
URL can be an external URL, such as **http://example.org/transcript.srt**, or - The name of the transcript file that's used in the **Default Timed Transcript** field on the **Basic** tab. This field is auto-populated. You don't have to change this setting.
the URL for a file that you've uploaded to your **Files & Uploads** page, such * - **Download Transcript Allowed**
as **/static/example.srt**. - Specifies whether you want to allow students to download the timed transcript. If you set this value to **True**, a link to download the file appears below the video.
This setting is related to **Transcript Download Allowed**. By default, Studio creates a .txt transcript when you upload an .srt transcript. Students can download the .srt or .txt versions of the transcript when you set **Download Transcript Allowed** to **True**. If you want to provide the transcript for download in a different format as well, such as .pdf, upload a file to Studio by using the **Upload Handout** field.
* If you set **Transcript Download Allowed** to **True**, and you specify a * - **Downloadable Transcript URL**
file in the **Download Transcript** field, the file you've specified will be - The URL for a non-.srt version of the transcript file posted on the **Files & Uploads** page or on the Internet. Students see a link to download the non-.srt transcript below the video.
available for students to download.
.. note:: When you add a transcript to this field, only the transcript that you add is available for download. The .srt and .txt transcripts become unavailable. If you want to provide a downloadable transcript in a format other than .srt, we recommend that you upload a handout for students by using the **Upload Handout** field. For more information, see :ref:`Additional Transcripts`.
* If you set **Transcript Download Allowed** to **True**, but you leave the
**Download Transcript** field blank, the .srt transcript that automatically * - **Show Transcript**
plays with the video will be available. - Specifies whether the transcript plays along with the video by default.
* - **Transcript Languages**
* **End Time**: The time, formatted as hours, minutes, and seconds (HH:MM:SS), - The transcript files for any additional languages. For more information, see :ref:`Transcripts in Additional Languages`.
when you want the video to end. * - **Upload Handout**
- Allows you to upload a handout to accompany this video. Your handout can be in any format. Students can download the handout by clicking **Download Handout** under the video.
* **Start Time**: The time, formatted as hours, minutes, and seconds (HH:MM:SS), * - **Video Download Allowed**
when you want the video to begin. - Specifies whether students can download versions of this video in different formats if they cannot use the edX video player or do not have access to YouTube. If you set this value to **True**, you must add at least one non-YouTube URL in the **Video File URLs** field.
* - **Video File URLs**
* **Transcript (primary)**: The name of the .srt file from the **Timed - The URL or URLs where you've posted non-YouTube versions of the video. Each URL must end in .mpeg, .mp4, .ogg, or .webm and cannot be a YouTube URL. Students will be able to view the first listed video that's compatible with the student's computer. To allow students to download these videos, you must set **Video Download Allowed** to **True**.
Transcript** field on the **Basic** tab. This field is auto-populated. You * - **Video Start Time**
don't have to change this setting. - The time you want the video to start if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59.
* - **Video Stop Time**
If your transcript uses an .sjson file, see :ref:`Steps for sjson files`. - The time you want the video to stop if you don't want the entire video to play. Formatted as HH:MM:SS. The maximum value is 23:59:59.
* - **YouTube ID, YouTube ID for .75x speed, YouTube ID for 1.25x speed, YouTube ID for 1.5x speed**
* **Transcript Display**: Specifies whether you want the transcript to show by - If you have uploaded separate videos to YouTube for different speeds of your video, enter the YouTube IDs for these videos in these fields. These settings are optional, for older browsers.
default. Students can always turn transcripts on or off while they watch the
video.
.. _Additional Transcripts:
* **Transcript Download Allowed**: Specifies whether you want to allow your **********************
students to download a copy of the transcript. Additional Transcripts
**********************
* **Transcript Translations**: The transcript files for any additional By default, a .txt file is created when you upload an .srt file, and students can download an .srt or .txt transcript when you set **Download Transcript Allowed** to **True**. The **Download Transcript** button appears below the video, and students see the .srt and .txt options when they hover over the button.
languages. For more information, see :ref:`Transcripts in Additional
Languages`.
* **Video Download Allowed**: Specifies whether you want to allow your students .. image:: /Images/Video_DownTrans_srt-txt.png
to download a copy of the video. :width: 500
:alt: Video status bar showing srt and txt transcript download options
* **Video Sources**: Additional locations where you've posted the video. This If you want to provide a downloadable transcript in a format such as .pdf along with the .srt and .txt transcripts, we recommend that you use the **Upload Handout** field. When you do this, a **Download Handout** button appears to the right of the **Download Transcript** button, and students can download the .srt, .txt, or handout version of the transcript.
field must contain a URL that ends in .mpeg, .mp4, .ogg, or .webm.
* **YouTube ID, YouTube ID for .75x speed, YouTube ID for 1.25x speed, YouTube .. image:: /Images/Video_DownTrans_srt-handout.png
ID for 1.5x speed**: If you have uploaded separate videos to YouTube for :width: 500
different speeds of your video, enter the YouTube IDs for these videos in :alt: Video status bar showing srt, txt, and handout transcript download options
these fields.
To add a downloadable transcript by using the **Upload Handout** field:
.. _Additional Transcripts: #. Create or obtain your transcript as a .pdf or in another format.
#. In the Video component, click the **Advanced** tab.
********************** #. Locate **Upload Handout**, and then click **Upload**.
Additional Transcripts #. In the **Upload File** dialog box, click **Choose File**.
********************** #. In the dialog box, select the file on your computer, and then click **Open**.
#. In the **Upload File** dialog box, click **Upload**.
You can provide your students with a downloadable transcript in a format such as
.txt or .pdf in addition to the .srt transcript that plays along with the video.
#. Upload the .txt or .pdf transcript to the **Files & Uploads** page or host it Before Studio added the **Upload Handout** feature, some courses posted transcript files on the **Files & Uploads** page or on the Internet, and then added a link to those files in the Video component. **We no longer recommend this method.** When you use this method, the **Download Transcript** button appears, but only the transcript that you add is available for download. The .srt and .txt transcripts become unavailable.
on an external website.
#. In the Video component, click the **Advanced** tab. .. image:: /Images/Video_DownTrans_other.png
:width: 500
:alt: Video status bar showing Download Transcript button without srt and txt options
#. In the **Download Transcript** field, enter the URL for the transcript. For If you want to use this method, you can post your transcript online, and then add the URL to the transcript in the **Downloadable Transcript URL** field. However, bear in mind that students will not be able to download .srt or .txt transcripts.
more information, see :ref:`Video Advanced Options`.
.. _Transcripts in Additional Languages: .. _Transcripts in Additional Languages:
...@@ -381,7 +377,7 @@ the Video component. ...@@ -381,7 +377,7 @@ the Video component.
#. Upload the .sjson file for your video to the **Files & Uploads** page. #. Upload the .sjson file for your video to the **Files & Uploads** page.
#. Create a new video component. #. Create a new video component.
#. On the **Basic** tab, enter the name that you want students to see in the #. On the **Basic** tab, enter the name that you want students to see in the
**Display Name** field. **Component Display Name** field.
#. In the **Video URL** field, enter the URL of the video. For example, the URL #. In the **Video URL** field, enter the URL of the video. For example, the URL
may resemble one of the following. may resemble one of the following.
...@@ -392,7 +388,7 @@ the Video component. ...@@ -392,7 +388,7 @@ the Video component.
https://s3.amazonaws.com/edx-course-videos/edx-edx101/EDXSPCPJSP13-G030300.mp4 https://s3.amazonaws.com/edx-course-videos/edx-edx101/EDXSPCPJSP13-G030300.mp4
#. Click the **Advanced** tab. #. Click the **Advanced** tab.
#. In the **Transcript (primary)** field, enter the file name of your video. Do #. In the **Default Timed Transcript** field, enter the file name of your video. Do
not include `subs_` or `.sjson`. For the example in step 2, you would only not include `subs_` or `.sjson`. For the example in step 2, you would only
enter **Lecture1a**. enter **Lecture1a**.
#. Set the other options that you want. #. Set the other options that you want.
......
...@@ -57,7 +57,7 @@ To create the above problem: ...@@ -57,7 +57,7 @@ To create the above problem:
</text> </text>
</problem> </problem>
.. _Drag and Drop Problem XML: .. _Problem with Adaptive Hint XML:
********************************* *********************************
Problem with Adaptive Hint XML Problem with Adaptive Hint XML
......
...@@ -10,7 +10,7 @@ The VitalSource Bookshelf e-reader tool provides your students with easy access ...@@ -10,7 +10,7 @@ The VitalSource Bookshelf e-reader tool provides your students with easy access
:width: 500 :width: 500
:alt: VitalSource e-book with highlighted note :alt: VitalSource e-book with highlighted note
For more information about Vital Source and its features, visit the `VitalSource Bookshelf support site <https://support.vitalsource.com/hc/en-us>`_. For more information about Vital Source and its features, visit the `VitalSource Bookshelf support site <https://support.vitalsource.com>`_.
.. note:: Before you add a VitalSource Bookshelf e-reader to your course, you must work with Vital Source to make sure the content you need already exists in the Vital Source inventory. If the content is not yet available, Vital Source works with the publisher of the e-book to create an e-book that meets the VitalSource Bookshelf specifications. **This process can take up to four months.** The following steps assume that the e-book you want is already part of the Vital Source inventory. .. note:: Before you add a VitalSource Bookshelf e-reader to your course, you must work with Vital Source to make sure the content you need already exists in the Vital Source inventory. If the content is not yet available, Vital Source works with the publisher of the e-book to create an e-book that meets the VitalSource Bookshelf specifications. **This process can take up to four months.** The following steps assume that the e-book you want is already part of the Vital Source inventory.
......
...@@ -194,9 +194,9 @@ When you add beta testers, note the following. ...@@ -194,9 +194,9 @@ When you add beta testers, note the following.
.. _Add_Testers_Bulk: .. _Add_Testers_Bulk:
-------------------------- ================================
Add Multiple Beta Testers Add Multiple Beta Testers
-------------------------- ================================
If you have a number of beta testers that you want to add, you can use the "batch If you have a number of beta testers that you want to add, you can use the "batch
add" option to add them all at once, rather than individually. With this add" option to add them all at once, rather than individually. With this
...@@ -229,9 +229,9 @@ testers**. ...@@ -229,9 +229,9 @@ testers**.
.. note:: The **Auto Enroll** option has no effect when you click **Remove beta testers**. The user's role as a beta tester is removed; course enrollment is not affected. .. note:: The **Auto Enroll** option has no effect when you click **Remove beta testers**. The user's role as a beta tester is removed; course enrollment is not affected.
----------------------------- ================================
Add Beta Testers Individually Add Beta Testers Individually
----------------------------- ================================
To add a single beta tester: To add a single beta tester:
......
###################################
May 15, 2014
###################################
The following information reflects what is new in the edX Platform as of May 15, 2014. See previous pages in this document for a history of changes.
**************************
edX Documentation
**************************
You can access the `edX Status`_ page to get an up-to-date status for all
services on edx.org and edX Edge. The page also includes the Twitter feed for
@edXstatus, which the edX Operations team uses to post updates.
You can access the public `edX roadmap`_ for
details about the currently planned product direction.
The following documentation is available:
* `Building and Running an edX Course`_
You can also download the guide as a PDF from the edX Studio user interface.
Recent changes include:
* Updated the `Running Your Course`_ chapter to remove references to the “new
beta” Instructor Dashboard.
* Updated `Enrollment`_ section to reflect that usernames or email
addresses can be used to batch enroll students.
* Updated `Grade and Answer Data`_ section to include new features in
the problem **Staff Debug** viewer for rescoring, resetting attempts, and
deleting state for a specified student.
* Updated `Staffing`_ section to explain the labeling differences
between Studio and the LMS with respect to course team roles.
* Updated `Assign Discussion Administration Roles`_ section to include a note
about course staff requiring explicit granting of discussion administration
roles.
* Added the `VitalSource E-Reader Tool`_ section.
* Updated `Add Files to a Course`_ section to include warnings about
file size.
* Updated the `LTI Component`_ section to reflect new settings.
* `edX Data Documentation`_
Recent changes include:
Updated `Tracking Logs`_ section to include events for course
enrollment activities: ``edx.course.enrollment.activated`` and
``edx.course.enrollment.deactivated``.
* `edX Platform Developer Documentation`_
Recent changes include:
Added an `Analytics`_ section for developers.
* `edX XBlock Documentation`_
*************
edX Studio
*************
* A problem that prevented you from hiding the Wiki in the list of Pages when
using Firefox is resolved. (STUD-1581)
* A problem that prevented you from importing a course created on edx.org into
edX Edge is resolved. (STUD-1599)
* All text in the Video component UI has been updated for clarity. (DOC-206)
***************************************
edX Learning Management System
***************************************
* The Instructor Dashboard that appears to course teams by default in the
LMS has changed. The Instructor Dashboard that appears when you click
**Instructor** is now the "New Beta" dashboard. The "Standard" dashboard
remains available; a button click is required to access it. The two dashboard
versions are also relabeled in this release. The version that was previously
identified as the "New Beta Dashboard" is now labeled "Instructor Dashboard",
and the version previously identified as the "Standard Dashboard" is now
labeled "Legacy Dashboard". (LMS-1296)
* Previously, when a student clicked **Run Code** for a MatLab problem, the
entire page was reloaded. This issue has been resolved so that now only the
MatLab problem elements are reloaded. (LMS-2505)
****************
edX Analytics
****************
* There is a new event tracking API for instrumenting events to capture user
actions and other point-in-time activities in Studio and the edX LMS. See
`Analytics`_ for more information.
.. include:: links.rst
\ No newline at end of file
...@@ -19,6 +19,7 @@ There is a page in this document for each update to the edX system on `edx.org`_ ...@@ -19,6 +19,7 @@ There is a page in this document for each update to the edX system on `edx.org`_
:maxdepth: 1 :maxdepth: 1
read_me read_me
05-15-2014
05-12-2014 05-12-2014
04-29-2014 04-29-2014
04-23-2014 04-23-2014
......
...@@ -150,6 +150,13 @@ ...@@ -150,6 +150,13 @@
.. _Drag and Drop Problem: http://ca.readthedocs.org/en/latest/exercises_tools/drag_and_drop.html .. _Drag and Drop Problem: http://ca.readthedocs.org/en/latest/exercises_tools/drag_and_drop.html
.. _Assign Discussion Administration Roles: http://edx.readthedocs.org/projects/ca/en/latest/running_course/discussions.html#assigning-discussion-roles
.. _LTI Component: http://edx.readthedocs.org/projects/ca/en/latest/exercises_tools/lti_component.html
.. _VitalSource E-Reader Tool: http://edx.readthedocs.org/projects/ca/en/latest/exercises_tools/vitalsource.html
.. DATA DOCUMENTATION .. DATA DOCUMENTATION
.. _Student Info and Progress Data: http://edx.readthedocs.org/projects/devdata/en/latest/internal_data_formats/sql_schema.html#student-info .. _Student Info and Progress Data: http://edx.readthedocs.org/projects/devdata/en/latest/internal_data_formats/sql_schema.html#student-info
...@@ -172,4 +179,6 @@ ...@@ -172,4 +179,6 @@
.. _Contributing to Open edX: http://edx.readthedocs.org/projects/userdocs/en/latest/process/index.html .. _Contributing to Open edX: http://edx.readthedocs.org/projects/userdocs/en/latest/process/index.html
.. _edX XBlock Documentation: http://edx.readthedocs.org/projects/xblock/en/latest/ .. _edX XBlock Documentation: http://edx.readthedocs.org/projects/xblock/en/latest/
\ No newline at end of file
.. _Analytics: http://edx.readthedocs.org/projects/userdocs/en/latest/analytics.html
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Computes the data to display on the Instructor Dashboard Computes the data to display on the Instructor Dashboard
""" """
from util.json_request import JsonResponse from util.json_request import JsonResponse
import json
from courseware import models from courseware import models
from django.db.models import Count from django.db.models import Count
...@@ -22,9 +23,12 @@ def get_problem_grade_distribution(course_id): ...@@ -22,9 +23,12 @@ def get_problem_grade_distribution(course_id):
`course_id` the course ID for the course interested in `course_id` the course ID for the course interested in
Output is a dict, where the key is the problem 'module_id' and the value is a dict with: Output is 2 dicts:
'prob-grade_distrib' where the key is the problem 'module_id' and the value is a dict with:
'max_grade' - max grade for this problem 'max_grade' - max grade for this problem
'grade_distrib' - array of tuples (`grade`,`count`). 'grade_distrib' - array of tuples (`grade`,`count`).
'total_student_count' where the key is problem 'module_id' and the value is number of students
attempting the problem
""" """
# Aggregate query on studentmodule table for grade data for all problems in course # Aggregate query on studentmodule table for grade data for all problems in course
...@@ -35,6 +39,7 @@ def get_problem_grade_distribution(course_id): ...@@ -35,6 +39,7 @@ def get_problem_grade_distribution(course_id):
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade')) ).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
prob_grade_distrib = {} prob_grade_distrib = {}
total_student_count = {}
# Loop through resultset building data for each problem # Loop through resultset building data for each problem
for row in db_query: for row in db_query:
...@@ -54,7 +59,10 @@ def get_problem_grade_distribution(course_id): ...@@ -54,7 +59,10 @@ def get_problem_grade_distribution(course_id):
'grade_distrib': [(row['grade'], row['count_grade'])] 'grade_distrib': [(row['grade'], row['count_grade'])]
} }
return prob_grade_distrib # Build set of total students attempting each problem
total_student_count[curr_problem] = total_student_count.get(curr_problem, 0) + row['count_grade']
return prob_grade_distrib, total_student_count
def get_sequential_open_distrib(course_id): def get_sequential_open_distrib(course_id):
...@@ -139,7 +147,7 @@ def get_d3_problem_grade_distrib(course_id): ...@@ -139,7 +147,7 @@ def get_d3_problem_grade_distrib(course_id):
'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem 'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem
""" """
prob_grade_distrib = get_problem_grade_distribution(course_id) prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id)
d3_data = [] d3_data = []
# Retrieve course object down to problems # Retrieve course object down to problems
...@@ -181,19 +189,24 @@ def get_d3_problem_grade_distrib(course_id): ...@@ -181,19 +189,24 @@ def get_d3_problem_grade_distrib(course_id):
for (grade, count_grade) in problem_info['grade_distrib']: for (grade, count_grade) in problem_info['grade_distrib']:
percent = 0.0 percent = 0.0
if max_grade > 0: if max_grade > 0:
percent = (grade * 100.0) / max_grade percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view # Compute percent of students with this grade
tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( student_count_percent = 0
label=label, if total_student_count.get(child.location.url(), 0) > 0:
problem_name=problem_name, student_count_percent = count_grade * 100 / total_student_count[child.location.url()]
count_grade=count_grade,
students=_("students"), # Tooltip parameters for problem in grade distribution view
percent=percent, tooltip = {
grade=grade, 'type': 'problem',
max_grade=max_grade, 'label': label,
questions=_("questions"), 'problem_name': problem_name,
) 'count_grade': count_grade,
'percent': percent,
'grade': grade,
'max_grade': max_grade,
'student_count_percent': student_count_percent,
}
# Construct data to be sent to d3 # Construct data to be sent to d3
stack_data.append({ stack_data.append({
...@@ -249,11 +262,14 @@ def get_d3_sequential_open_distrib(course_id): ...@@ -249,11 +262,14 @@ def get_d3_sequential_open_distrib(course_id):
num_students = sequential_open_distrib[subsection.location] num_students = sequential_open_distrib[subsection.location]
stack_data = [] stack_data = []
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
num_students=num_students, # Tooltip parameters for subsection in open_distribution view
subsection_num=c_subsection, tooltip = {
subsection_name=subsection_name, 'type': 'subsection',
) 'num_students': num_students,
'subsection_num': c_subsection,
'subsection_name': subsection_name
}
stack_data.append({ stack_data.append({
'color': 0, 'color': 0,
...@@ -332,19 +348,18 @@ def get_d3_section_grade_distrib(course_id, section): ...@@ -332,19 +348,18 @@ def get_d3_section_grade_distrib(course_id, section):
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']: for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
percent = 0.0 percent = 0.0
if max_grade > 0: if max_grade > 0:
percent = (grade * 100.0) / max_grade percent = round((grade * 100.0) / max_grade, 1)
# Construct tooltip for problem in grade distibution view # Construct tooltip for problem in grade distibution view
tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( tooltip = {
problem_info_x=problem_info[problem]['x_value'], 'type': 'problem',
count_grade=count_grade, 'problem_info_x': problem_info[problem]['x_value'],
students=_("students"), 'count_grade': count_grade,
percent=percent, 'percent': percent,
problem_info_n=problem_info[problem]['display_name'], 'problem_info_n': problem_info[problem]['display_name'],
grade=grade, 'grade': grade,
max_grade=max_grade, 'max_grade': max_grade,
questions=_("questions"), }
)
stack_data.append({ stack_data.append({
'color': percent, 'color': percent,
...@@ -418,7 +433,12 @@ def get_students_opened_subsection(request, csv=False): ...@@ -418,7 +433,12 @@ def get_students_opened_subsection(request, csv=False):
If 'csv' is True, returns a header array, and an array of arrays in the format: If 'csv' is True, returns a header array, and an array of arrays in the format:
student names, usernames for CSV download. student names, usernames for CSV download.
""" """
<<<<<<< HEAD
module_state_key = Location.from_deprecated_string(request.GET.get('module_id')) module_state_key = Location.from_deprecated_string(request.GET.get('module_id'))
=======
module_id = request.GET.get('module_id')
>>>>>>> edx/master
csv = request.GET.get('csv') csv = request.GET.get('csv')
# Query for "opened a subsection" students # Query for "opened a subsection" students
...@@ -450,9 +470,11 @@ def get_students_opened_subsection(request, csv=False): ...@@ -450,9 +470,11 @@ def get_students_opened_subsection(request, csv=False):
return JsonResponse(response_payload) return JsonResponse(response_payload)
else: else:
tooltip = request.GET.get('tooltip') tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[tooltip.index('S'):])
header = ['Name', 'Username'] # Subsection name is everything after 3rd space in tooltip
filename = sanitize_filename(' '.join(tooltip.split(' ')[3:]))
header = [_("Name").encode('utf-8'), _("Username").encode('utf-8')]
for student in students: for student in students:
results.append([student['student__profile__name'], student['student__username']]) results.append([student['student__profile__name'], student['student__username']])
...@@ -510,7 +532,7 @@ def get_students_problem_grades(request, csv=False): ...@@ -510,7 +532,7 @@ def get_students_problem_grades(request, csv=False):
tooltip = request.GET.get('tooltip') tooltip = request.GET.get('tooltip')
filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')]) filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')])
header = ['Name', 'Username', 'Grade', 'Percent'] header = [_("Name").encode('utf-8'), _("Username").encode('utf-8'), _("Grade").encode('utf-8'), _("Percent").encode('utf-8')]
for student in students: for student in students:
percent = 0 percent = 0
...@@ -522,11 +544,60 @@ def get_students_problem_grades(request, csv=False): ...@@ -522,11 +544,60 @@ def get_students_problem_grades(request, csv=False):
return response return response
def post_metrics_data_csv(request):
"""
Generate a list of opened subsections or problems for the entire course for CSV download.
Returns a header array, and an array of arrays in the format:
section, subsection, count of students for subsections
or section, problem, name, count of students, percent of students, score for problems.
"""
data = json.loads(request.POST['data'])
sections = json.loads(data['sections'])
tooltips = json.loads(data['tooltips'])
course_id = data['course_id']
data_type = data['data_type']
results = []
if data_type == 'subsection':
header = [_("Section").encode('utf-8'), _("Subsection").encode('utf-8'), _("Opened by this number of students").encode('utf-8')]
filename = sanitize_filename(_('subsections') + '_' + course_id)
elif data_type == 'problem':
header = [_("Section").encode('utf-8'), _("Problem").encode('utf-8'), _("Name").encode('utf-8'), _("Count of Students").encode('utf-8'), _("% of Students").encode('utf-8'), _("Score").encode('utf-8')]
filename = sanitize_filename(_('problems') + '_' + course_id)
for index, section in enumerate(sections):
results.append([section])
# tooltips array is array of dicts for subsections and
# array of array of dicts for problems.
if data_type == 'subsection':
for tooltip_dict in tooltips[index]:
num_students = tooltip_dict['num_students']
subsection = tooltip_dict['subsection_name']
# Append to results offsetting 1 column to the right.
results.append(['', subsection, num_students])
elif data_type == 'problem':
for tooltip in tooltips[index]:
for tooltip_dict in tooltip:
label = tooltip_dict['label']
problem_name = tooltip_dict['problem_name']
count_grade = tooltip_dict['count_grade']
student_count_percent = tooltip_dict['student_count_percent']
percent = tooltip_dict['percent']
# Append to results offsetting 1 column to the right.
results.append(['', label, problem_name, count_grade, student_count_percent, percent])
response = create_csv_response(filename, header, results)
return response
def sanitize_filename(filename): def sanitize_filename(filename):
""" """
Utility function Utility function
""" """
filename = filename.replace(" ", "_") filename = filename.replace(" ", "_")
filename = filename.encode('ascii') filename = filename.encode('utf-8')
filename = filename[0:25] + '.csv' filename = filename[0:25] + '.csv'
return filename return filename
...@@ -94,12 +94,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -94,12 +94,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
def test_get_problem_grade_distribution(self): def test_get_problem_grade_distribution(self):
prob_grade_distrib = get_problem_grade_distribution(self.course.id) prob_grade_distrib, total_student_count = get_problem_grade_distribution(self.course.id)
for problem in prob_grade_distrib: for problem in prob_grade_distrib:
max_grade = prob_grade_distrib[problem]['max_grade'] max_grade = prob_grade_distrib[problem]['max_grade']
self.assertEquals(1, max_grade) self.assertEquals(1, max_grade)
for val in total_student_count.values():
self.assertEquals(USER_COUNT, val)
def test_get_sequential_open_distibution(self): def test_get_sequential_open_distibution(self):
sequential_open_distrib = get_sequential_open_distrib(self.course.id) sequential_open_distrib = get_sequential_open_distrib(self.course.id)
...@@ -242,6 +245,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): ...@@ -242,6 +245,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase):
# Check response contains 1 line for each user +1 for the header # Check response contains 1 line for each user +1 for the header
self.assertEquals(USER_COUNT + 1, len(response.content.splitlines())) self.assertEquals(USER_COUNT + 1, len(response.content.splitlines()))
def test_post_metrics_data_subsections_csv(self):
url = reverse('post_metrics_data_csv')
sections = json.dumps(["Introduction"])
tooltips = json.dumps([[{"subsection_name": "Pre-Course Survey", "subsection_num": 1, "type": "subsection", "num_students": 18963}]])
course_id = self.course.id
data_type = 'subsection'
data = json.dumps({'sections': sections,
'tooltips': tooltips,
'course_id': course_id,
'data_type': data_type,
})
response = self.client.post(url, {'data': data})
# Check response contains 1 line for header, 1 line for Section and 1 line for Subsection
self.assertEquals(3, len(response.content.splitlines()))
def test_post_metrics_data_problems_csv(self):
url = reverse('post_metrics_data_csv')
sections = json.dumps(["Introduction"])
tooltips = json.dumps([[[
{'student_count_percent': 0,
'problem_name': 'Q1',
'grade': 0,
'percent': 0,
'label': 'P1.2.1',
'max_grade': 1,
'count_grade': 26,
'type': u'problem'},
{'student_count_percent': 99,
'problem_name': 'Q1',
'grade': 1,
'percent': 100,
'label': 'P1.2.1',
'max_grade': 1,
'count_grade': 4763,
'type': 'problem'},
]]])
course_id = self.course.id
data_type = 'problem'
data = json.dumps({'sections': sections,
'tooltips': tooltips,
'course_id': course_id,
'data_type': data_type,
})
response = self.client.post(url, {'data': data})
# Check response contains 1 line for header, 1 line for Sections and 2 lines for problems
self.assertEquals(4, len(response.content.splitlines()))
def test_get_section_display_name(self): def test_get_section_display_name(self):
section_display_name = get_section_display_name(self.course.id) section_display_name = get_section_display_name(self.course.id)
......
"""
Class Dashboard API endpoint urls.
"""
from django.conf.urls import patterns, url
urlpatterns = patterns('', # nopep8
# Json request data for metrics for entire course
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
'class_dashboard.views.all_problem_grade_distribution', name="all_problem_grade_distribution"),
# Json request data for metrics for particular section
url(r'^(?P<course_id>[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P<section>\d+)$',
'class_dashboard.views.section_problem_grade_distrib', name="section_problem_grade_distrib"),
# For listing students that opened a sub-section
url(r'^get_students_opened_subsection$',
'class_dashboard.dashboard_data.get_students_opened_subsection', name="get_students_opened_subsection"),
# For listing of students' grade per problem
url(r'^get_students_problem_grades$',
'class_dashboard.dashboard_data.get_students_problem_grades', name="get_students_problem_grades"),
# For generating metrics data as a csv
url(r'^post_metrics_data_csv_url',
'class_dashboard.dashboard_data.post_metrics_data_csv', name="post_metrics_data_csv"),
)
...@@ -255,10 +255,17 @@ def _section_metrics(course_key, access): ...@@ -255,10 +255,17 @@ def _section_metrics(course_key, access):
'section_key': 'metrics', 'section_key': 'metrics',
'section_display_name': ('Metrics'), 'section_display_name': ('Metrics'),
'access': access, 'access': access,
<<<<<<< HEAD
'sub_section_display_name': get_section_display_name(course_key), 'sub_section_display_name': get_section_display_name(course_key),
'section_has_problem': get_array_section_has_problem(course_key), 'section_has_problem': get_array_section_has_problem(course_key),
=======
'course_id': course_id,
'sub_section_display_name': get_section_display_name(course_id),
'section_has_problem': get_array_section_has_problem(course_id),
>>>>>>> edx/master
'get_students_opened_subsection_url': reverse('get_students_opened_subsection'), 'get_students_opened_subsection_url': reverse('get_students_opened_subsection'),
'get_students_problem_grades_url': reverse('get_students_problem_grades'), 'get_students_problem_grades_url': reverse('get_students_problem_grades'),
'post_metrics_data_csv_url': reverse('post_metrics_data_csv'),
} }
return section_data return section_data
......
...@@ -4,7 +4,6 @@ from edxmako.shortcuts import render_to_response ...@@ -4,7 +4,6 @@ from edxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from notes.models import Note from notes.models import Note
from notes.utils import notes_enabled_for_course from notes.utils import notes_enabled_for_course
from xmodule.annotator_token import retrieve_token
@login_required @login_required
...@@ -23,8 +22,7 @@ def notes(request, course_id): ...@@ -23,8 +22,7 @@ def notes(request, course_id):
'course': course, 'course': course,
'notes': notes, 'notes': notes,
'student': student, 'student': student,
'storage': storage, 'storage': storage
'token': retrieve_token(student.email, course.annotation_token_secret),
} }
return render_to_response('notes.html', context) return render_to_response('notes.html', context)
...@@ -828,7 +828,6 @@ main_vendor_js = [ ...@@ -828,7 +828,6 @@ main_vendor_js = [
'js/vendor/swfobject/swfobject.js', 'js/vendor/swfobject/swfobject.js',
'js/vendor/jquery.ba-bbq.min.js', 'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/ova/annotator-full.js', 'js/vendor/ova/annotator-full.js',
'js/vendor/ova/annotator-full-firebase-auth.js',
'js/vendor/ova/video.dev.js', 'js/vendor/ova/video.dev.js',
'js/vendor/ova/vjs.youtube.js', 'js/vendor/ova/vjs.youtube.js',
'js/vendor/ova/rangeslider.js', 'js/vendor/ova/rangeslider.js',
......
...@@ -114,6 +114,7 @@ var StaffDebug = (function(){ ...@@ -114,6 +114,7 @@ var StaffDebug = (function(){
// Register click handlers // Register click handlers
$(document).ready(function() { $(document).ready(function() {
<<<<<<< HEAD
$('#staff-debug-reset').click(function() { $('#staff-debug-reset').click(function() {
StaffDebug.reset($(this).parent().data('location-name'), $(this).parent().data('location')); StaffDebug.reset($(this).parent().data('location-name'), $(this).parent().data('location'));
return false; return false;
...@@ -124,6 +125,18 @@ $(document).ready(function() { ...@@ -124,6 +125,18 @@ $(document).ready(function() {
}); });
$('#staff-debug-rescore').click(function() { $('#staff-debug-rescore').click(function() {
StaffDebug.rescore($(this).parent().data('location-name'), $(this).parent().data('location')); StaffDebug.rescore($(this).parent().data('location-name'), $(this).parent().data('location'));
=======
$('.staff-debug-reset').click(function() {
StaffDebug.reset($(this).data('location'));
return false;
});
$('.staff-debug-sdelete').click(function() {
StaffDebug.sdelete($(this).data('location'));
return false;
});
$('.staff-debug-rescore').click(function() {
StaffDebug.rescore($(this).data('location'));
>>>>>>> edx/master
return false; return false;
}); });
}); });
...@@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 { ...@@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 {
.instructor-dashboard-wrapper-2 section.idash-section#metrics { .instructor-dashboard-wrapper-2 section.idash-section#metrics {
.metrics-container { .metrics-container, .metrics-header-container {
position: relative; position: relative;
width: 100%; width: 100%;
float: left; float: left;
clear: both; clear: both;
margin-top: 25px; margin-top: 25px;
.metrics-left { .metrics-left, .metrics-left-header {
position: relative; position: relative;
width: 30%; width: 30%;
height: 640px;
float: left; float: left;
margin-right: 2.5%; margin-right: 2.5%;
...@@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 { ...@@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 {
width: 100%; width: 100%;
} }
} }
.metrics-right { .metrics-section.metrics-left {
height: 640px;
}
.metrics-right, .metrics-right-header {
position: relative; position: relative;
width: 65%; width: 65%;
height: 295px;
float: left; float: left;
margin-left: 2.5%; margin-left: 2.5%;
margin-bottom: 25px; margin-bottom: 25px;
...@@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 { ...@@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 {
} }
} }
.metrics-section.metrics-right {
height: 295px;
}
svg { svg {
.stacked-bar { .stacked-bar {
cursor: pointer; cursor: pointer;
...@@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 { ...@@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 {
border-radius: 5px; border-radius: 5px;
margin-top: 25px; margin-top: 25px;
} }
input#graph_reload {
display: none;
}
} }
} }
......
<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, **kwargs"/> <%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, allSubsectionTooltipArr, allProblemTooltipArr, **kwargs"/>
<%! <%!
import json import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -30,6 +30,13 @@ $(function () { ...@@ -30,6 +30,13 @@ $(function () {
margin: {left:0}, margin: {left:0},
}; };
// Construct array of tooltips for all sections for the "Download Subsection Data" button.
var sectionTooltipArr = new Array();
paramOpened.data.forEach( function(element, index, array) {
sectionTooltipArr[index] = element.stackData[0].tooltip;
});
allSubsectionTooltipArr[i] = sectionTooltipArr;
barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"), barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i)); d3.select("#${id_tooltip_prefix}"+i));
barGraphOpened.scale.stackColor.range(["#555555","#555555"]); barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
...@@ -68,6 +75,17 @@ $(function () { ...@@ -68,6 +75,17 @@ $(function () {
bVerticalXAxisLabel : true, bVerticalXAxisLabel : true,
}; };
// Construct array of tooltips for all sections for the "Download Problem Data" button.
var sectionTooltipArr = new Array();
paramGrade.data.forEach( function(element, index, array) {
var stackDataArr = new Array();
for (var j = 0; j < element.stackData.length; j++) {
stackDataArr[j] = element.stackData[j].tooltip
}
sectionTooltipArr[index] = stackDataArr;
});
allProblemTooltipArr[i] = sectionTooltipArr;
barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"), barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"),
d3.select("#${id_tooltip_prefix}"+i)); d3.select("#${id_tooltip_prefix}"+i));
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]); barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
...@@ -83,6 +101,7 @@ $(function () { ...@@ -83,6 +101,7 @@ $(function () {
i+=1; i+=1;
} }
}); });
}); });
\ No newline at end of file
...@@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) { ...@@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
var top = pos[1]-10; var top = pos[1]-10;
var width = $('#'+graph.divTooltip.attr("id")).width(); var width = $('#'+graph.divTooltip.attr("id")).width();
// Construct the tooltip
if (d.tooltip['type'] == 'subsection') {
tooltip_str = d.tooltip['num_students'] + ' ' + gettext('student(s) opened Subsection') + ' ' \
+ d.tooltip['subsection_num'] + ': ' + d.tooltip['subsection_name']
}else if (d.tooltip['type'] == 'problem') {
tooltip_str = d.tooltip['label'] + ' ' + d.tooltip['problem_name'] + ' - ' \
+ d.tooltip['count_grade'] + ' ' + gettext('students') + ' (' \
+ d.tooltip['student_count_percent'] + '%) (' + \
+ d.tooltip['percent'] + '%: ' + \
+ d.tooltip['grade'] +'/' + d.tooltip['max_grade'] + ' '
+ gettext('questions') + ')'
}
graph.divTooltip.style("visibility", "visible") graph.divTooltip.style("visibility", "visible")
.text(d.tooltip); .text(tooltip_str);
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width()) if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
left -= (width+30); left -= (width+30);
......
...@@ -17,6 +17,13 @@ ...@@ -17,6 +17,13 @@
<%static:css group='style-vendor-tinymce-skin'/> <%static:css group='style-vendor-tinymce-skin'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<script type="text/javascript">
// This is a hack to get tinymce to work correctly in Firefox until the annotator tool is refactored to not include
// tinymce globally.
if(typeof window.Range.prototype === "undefined") {
window.Range.prototype = { };
}
</script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
...@@ -715,7 +722,9 @@ function goto( mode) ...@@ -715,7 +722,9 @@ function goto( mode)
</div> </div>
%endfor %endfor
<script> <script>
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)} var allSubsectionTooltipArr = new Array();
var allProblemTooltipArr = new Array();
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
</script> </script>
%endif %endif
......
...@@ -25,6 +25,13 @@ ...@@ -25,6 +25,13 @@
<%static:css group='style-vendor-tinymce-content'/> <%static:css group='style-vendor-tinymce-content'/>
<%static:css group='style-vendor-tinymce-skin'/> <%static:css group='style-vendor-tinymce-skin'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
<script type="text/javascript">
// This is a hack to get tinymce to work correctly in Firefox until the annotator tool is refactored to not include
// tinymce globally.
if(typeof window.Range.prototype === "undefined") {
window.Range.prototype = { };
}
</script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/> <%page args="section_data"/>
...@@ -11,19 +11,35 @@ ...@@ -11,19 +11,35 @@
%else: %else:
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/> <%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/>
<%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/> <%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
<div id="graph_reload">
<h3 class="attention" id="graph_load">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3> <p>${_("Use Reload Graphs to refresh the graphs.")}</p>
<input type="button" id="graph_reload" value="${_("Reload Graphs")}" /> <p><input type="button" value="${_("Reload Graphs")}"/></p>
</div>
<div class="metrics-header-container">
<div class="metrics-left-header">
<h2>${_("Subsection Data")}</h2>
<p>${_("Each bar shows the number of students that opened the subsection.")}</p>
<p>${_("You can click on any of the bars to list the students that opened the subsection.")}</p>
<p>${_("You can also download this data as a CSV file.")}</p>
<p><input type="button" id="download_subsection_data" value="${_("Download Subsection Data for all Subsections as a CSV")}" /></p>
</div>
<div class="metrics-right-header">
<h2>${_("Grade Distribution Data")}</h2>
<p>${_("Each bar shows the grade distribution for that problem.")}</p>
<p>${_("You can click on any of the bars to list the students that attempted the problem, along with the grades they received.")}</p>
<p>${_("You can also download this data as a CSV file.")}</p>
<p><input type="button" id="download_problem_data" value="${_("Download Problem Data for all Problems as a CSV")}" /></p>
</div>
</div>
<!-- For each section with data, create the divs for displaying the graphs <!-- For each section with data, create the divs for displaying the graphs
and the popup window for listing the students and the popup window for listing the students
--> -->
%for i in range(0, len(section_data['sub_section_display_name'])): %for i in range(0, len(section_data['sub_section_display_name'])):
<div class="metrics-container" id="metrics_section_${i}"> <div class="metrics-container" id="metrics_section_${i}">
<h2>${_("Section:")} ${section_data['sub_section_display_name'][i]}</h2> <h2>${_("Section")}: ${section_data['sub_section_display_name'][i]}</h2>
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div> <div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
<div class="metrics-section metrics-left" id="metric_opened_${i}"> <div class="metrics-section metrics-left" id="metric_opened_${i}">
<h3>${_("Count of Students Opened a Subsection")}</h3>
</div> </div>
<div class="metrics-section metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}> <div class="metrics-section metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}>
<h3>${_("Grade Distribution per Problem")}</h3> <h3>${_("Grade Distribution per Problem")}</h3>
...@@ -46,11 +62,91 @@ ...@@ -46,11 +62,91 @@
<script> <script>
$(function () { $(function () {
var firstLoad = true; var firstLoad = true;
var allSubsectionTooltipArr = new Array();
var allProblemTooltipArr = new Array();
// Click handler for left bars
$('.metrics-container').on("click", '.metrics-left .stacked-bar', function () {
var module_id = $('rect', this).attr('id');
var metrics_overlay = $(this).closest('.metrics-left').siblings('.metrics-overlay');
// Set module_id attribute on metrics_overlay
metrics_overlay.data("module-id", module_id);
var header = $(this).closest('.metrics-left').siblings('.metrics-tooltip').text();
var overlay_content = '<h3 class="metrics-overlay-title">' + header + '</h3>';
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
$.ajax({
url: "${section_data['get_students_opened_subsection_url']}",
type: "GET",
data: {module_id: module_id},
dataType: "json",
success: function(response) {
overlay_content = "<tr class='header'><th>${_('Name')}</th><th>${_('Username')}</th></tr>";
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
$.each(response.results, function(index, value ){
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + '</td></tr>';
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
});
// If student list too long, append message to screen.
if (response.max_exceeded) {
overlay_content = "<p class='overflow-message'>${_('This is a partial list, to view all students download as a csv.')}</p>";
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
}
}
})
metrics_overlay.find('.metrics-student-opened').show();
metrics_overlay.show();
});
// Click handler for right bars
$('.metrics-container').on("click", '.metrics-right .stacked-bar', function () {
var module_id = $('rect', this).attr('id');
var metrics_overlay = $(this).closest('.metrics-right').siblings('.metrics-overlay');
//Set module_id attribute on metrics_overlay
metrics_overlay.data("module-id", module_id);
var header = $(this).closest('.metrics-right').siblings('.metrics-tooltip').text();
var far_index = header.indexOf(' - ');
var title = header.substring(0, far_index);
var overlay_content = '<h3 class="metrics-overlay-title">' + title + '</h3>';
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
$.ajax({
url: "${section_data['get_students_problem_grades_url']}",
type: "GET",
data: {module_id: module_id},
dataType: "json",
success: function(response) {
overlay_content = "<tr class='header'><th>${_('Name')}</th><th>${_('Username')}</th><th>${_('Grade')}</th><th>${_('Percent')}</th></tr>";
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
$.each(response.results, function(index, value ){
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + "</td><td>" + value['grade'] + "</td><td>" + value['percent'] + '</td></tr>';
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
});
// If student list too long, append message to screen.
if (response.max_exceeded) {
overlay_content = "<p class='overflow-message'>${_('This is a partial list, to view all students download as a csv.')}</p>";
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
}
},
})
metrics_overlay.find('.metrics-student-grades').show();
metrics_overlay.show();
});
loadGraphs = function() { loadGraphs = function() {
$('#graph_load').show();
$('#graph_reload').hide(); $('#graph_reload').hide();
$('.metrics-header-container').hide();
$('.loading').remove(); $('.loading').remove();
var nothingText = "${_('There are no problems in this section.')}"; var nothingText = "${_('There are no problems in this section.')}";
var loadingText = "${_('Loading...')}"; var loadingText = "${_('Loading...')}";
...@@ -71,103 +167,87 @@ ...@@ -71,103 +167,87 @@
}); });
$('.metrics-left svg, .metrics-right svg').remove(); $('.metrics-left svg, .metrics-right svg').remove();
${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id)} ${all_section_metrics.body("metric_opened_", "metric_grade_", "metric_attempts_", "metric_tooltip_", course.id, allSubsectionTooltipArr, allProblemTooltipArr)}
}
setTimeout(function() {
$('#graph_load, #graph_reload').toggle(); // For downloading subsection and problem data as csv
$('.metrics-left .stacked-bar').on("click", function () { download_csv_data = function(event) {
var module_id = $('rect', this).attr('id');
var metrics_overlay = $(this).closest('.metrics-left').siblings('.metrics-overlay'); var allSectionArr = []
var allTooltipArr = []
// Set module_id attribute on metrics_overlay if (event.type == 'subsection') {
metrics_overlay.data("module-id", module_id); allTooltipArr = allSubsectionTooltipArr;
} else if (event.type == 'problem') {
var header = $(this).closest('.metrics-left').siblings('.metrics-tooltip').text(); allTooltipArr = allProblemTooltipArr;
var overlay_content = '<h3 class="metrics-overlay-title">' + header + '</h3>'; }
$('.metrics-overlay-content', metrics_overlay).before(overlay_content); allTooltipArr.forEach( function(element, index, array) {
$.ajax({ var metrics_section = 'metrics_section' + '_' + index
url: "${section_data['get_students_opened_subsection_url']}", // Get Section heading which is everything after first ': '
type: "GET", var heading = $('#' + metrics_section).children('h2').text();
data: {module_id: module_id}, allSectionArr[index] = heading.substr(heading.indexOf(': ') +2)
dataType: "json", });
success: function(response) { var data = {}
overlay_content = '<tr class="header"><th>${_("Name")}</th><th>${_("Username")}</th></tr>'; data['sections'] = JSON.stringify(allSectionArr);
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content); data['tooltips'] = JSON.stringify(allTooltipArr);
data['course_id'] = "${section_data['course_id']}";
$.each(response.results, function(index, value ){ data['data_type'] = event.type;
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + '</td></tr>';
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content); var input_data = document.createElement("input");
}); input_data.name = 'data';
// If student list too long, append message to screen. input_data.value = JSON.stringify(data);
if (response.max_exceeded) {
overlay_content = '<p class="overflow-message">${_("This is a partial list, to view all students download as a csv.")}</p>'; var csrf_token_input = document.createElement("input");
$('.metrics-overlay-content', metrics_overlay).after(overlay_content); csrf_token_input.name = 'csrfmiddlewaretoken';
} csrf_token_input.value = "${ csrf_token }"
}
}) // Send data as a POST so it doesn't create a huge url
metrics_overlay.find('.metrics-student-opened').show(); var form = document.createElement("form");
metrics_overlay.show(); form.action = "${section_data['post_metrics_data_csv_url']}";
}); form.method = 'post'
$('.metrics-right .stacked-bar').on("click",function () { form.appendChild(input_data);
var module_id = $('rect', this).attr('id'); form.appendChild(csrf_token_input)
var metrics_overlay = $(this).closest('.metrics-right').siblings('.metrics-overlay');
document.body.appendChild(form);
//Set module_id attribute on metrics_overlay form.submit();
metrics_overlay.data("module-id", module_id);
var header = $(this).closest('.metrics-right').siblings('.metrics-tooltip').text();
var far_index = header.indexOf(' students (');
var near_index = header.substr(0, far_index).lastIndexOf(' ') + 1;
var title = header.substring(0, near_index -3);
var overlay_content = '<h3 class="metrics-overlay-title">' + title + '</h3>';
$('.metrics-overlay-content', metrics_overlay).before(overlay_content);
$.ajax({
url: "${section_data['get_students_problem_grades_url']}",
type: "GET",
data: {module_id: module_id},
dataType: "json",
success: function(response) {
overlay_content = '<tr class="header"><th>${_("Name")}</th><th>${_("Username")}</th><th>${_("Grade")}</th><th>${_("Percent")}</th></tr>';
$('.metrics-overlay-content thead', metrics_overlay).append(overlay_content);
$.each(response.results, function(index, value ){
overlay_content = '<tr><td>' + value['name'] + "</td><td>" + value['username'] + "</td><td>" + value['grade'] + "</td><td>" + value['percent'] + '</td></tr>';
$('.metrics-overlay-content tbody', metrics_overlay).append(overlay_content);
});
// If student list too long, append message to screen.
if (response.max_exceeded) {
overlay_content = '<p class="overflow-message">${_("This is a partial list, to view all students download as a csv.")}</p>';
$('.metrics-overlay-content', metrics_overlay).after(overlay_content);
}
},
})
metrics_overlay.find('.metrics-student-grades').show();
metrics_overlay.show();
});
}, 5000);
} }
$('.instructor-nav a').click(function () { $('.instructor-nav a').click(function () {
if ($(this).data('section') === "metrics" && firstLoad) { if ($(this).data('section') === "metrics" && firstLoad) {
loadGraphs(); loadGraphs();
firstLoad = false; firstLoad = false;
$('#graph_reload').show();
$('.metrics-header-container').show();
} }
}); });
$('#graph_reload').click(function () { $('#graph_reload').click(function () {
loadGraphs(); loadGraphs();
$('#graph_reload').show();
$('.metrics-header-container').show();
});
$('#download_subsection_data').click(function() {
download_csv_data({'type': 'subsection'});
});
$('#download_problem_data').click(function() {
download_csv_data({'type': 'problem'});
}); });
if (window.location.hash === "#view-metrics") { if (window.location.hash === "#view-metrics") {
$('.instructor-nav a[data-section="metrics"]').click(); $('.instructor-nav a[data-section="metrics"]').click();
$('#graph_reload').hide();
$('.metrics-header-container').hide();
} }
$(document).ajaxStop(function() {
$('#graph_reload').show();
$('.metrics-header-container').show();
});
}); });
$('.metrics-overlay .close-button').click(function(event) { $('.metrics-overlay .close-button').click(function(event) {
event.preventDefault(); event.preventDefault();
...@@ -179,13 +259,14 @@ ...@@ -179,13 +259,14 @@
}); });
$('.metrics-overlay .download-csv').click(function(event) { $('.metrics-overlay .download-csv').click(function(event) {
var module_id = $(this).closest('.metrics-overlay').data("module-id"); var module_id = $(this).closest('.metrics-overlay').data("module-id");
var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text(); var tooltip = $(this).closest('.metrics-container').children('.metrics-tooltip').text();
var attributes = '?module_id=' + module_id + '&tooltip=' + tooltip + '&csv=true'; var attributes = '?module_id=' + module_id + '&csv=true' + '&tooltip=' + tooltip;
var url = $(this).data("endpoint"); var url = $(this).data("endpoint");
url += attributes; url += attributes;
return location.href = url; return location.href = url;
}); });
</script> </script>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</p> </p>
<br> <br>
<p> <p>
<a href="${ section_data['spoc_gradebook_url'] }" class="progress-link"> ${_("View Gradebook")} </a> <a href="${ section_data['spoc_gradebook_url'] }" class="gradebook-link"> ${_("View Gradebook")} </a>
</p> </p>
<hr> <hr>
%endif %endif
......
...@@ -66,10 +66,19 @@ ...@@ -66,10 +66,19 @@
</section> </section>
<script> <script>
<<<<<<< HEAD
//Grab uri of the course //Grab uri of the course
var parts = window.location.href.split("/"), var parts = window.location.href.split("/"),
uri = ''; uri = '';
for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url
=======
//Grab uri of the course
var parts = window.location.href.split("/"),
uri = '',
courseid;
for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
>>>>>>> edx/master
var pagination = 100, var pagination = 100,
is_staff = false, is_staff = false,
options = { options = {
...@@ -168,7 +177,7 @@ ...@@ -168,7 +177,7 @@
}, },
}, },
auth: { auth: {
token: "${token}" tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
}, },
store: { store: {
// The endpoint of the store on your server. // The endpoint of the store on your server.
......
...@@ -63,12 +63,21 @@ ${block_content} ...@@ -63,12 +63,21 @@ ${block_content}
</div> </div>
<div data-location="${unicode(location)}" data-location-name="${location.name}"> <div data-location="${unicode(location)}" data-location-name="${location.name}">
[ [
<<<<<<< HEAD
<a href="#" id="staff-debug-reset">${_('Reset Student Attempts')}</a> <a href="#" id="staff-debug-reset">${_('Reset Student Attempts')}</a>
| |
<a href="#" id="staff-debug-sdelete">${_('Delete Student State')}</a> <a href="#" id="staff-debug-sdelete">${_('Delete Student State')}</a>
| |
<a href="#" id="staff-debug-rescore">${_('Rescore Student Submission')}</a> <a href="#" id="staff-debug-rescore">${_('Rescore Student Submission')}</a>
=======
<a href="#" id="staff-debug-reset" class="staff-debug-reset" data-location="${location.name}">${_('Reset Student Attempts')}</a>
|
<a href="#" id="staff-debug-sdelete" class="staff-debug-sdelete" data-location="${location.name}">${_('Delete Student State')}</a>
|
<a href="#" id="staff-debug-rescore" class="staff-debug-rescore" data-location="${location.name}">${_('Rescore Student Submission')}</a>
>>>>>>> edx/master
] ]
</div> </div>
<div id="result_${location.name}"/> <div id="result_${location.name}"/>
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<div class="annotatable-wrapper"> <div class="annotatable-wrapper">
<div class="annotatable-header"> <div class="annotatable-header">
% if display_name is not UNDEFINED and display_name is not None: % if display_name is not UNDEFINED and display_name is not None:
<div class="annotatable-title">${display_name}</div> <div class="annotatable-title">${display_name}</div>
% endif % endif
</div> </div>
% if instructions_html is not UNDEFINED and instructions_html is not None: % if instructions_html is not UNDEFINED and instructions_html is not None:
<div class="annotatable-section shaded"> <div class="annotatable-section shaded">
<div class="annotatable-section-title"> <div class="annotatable-section-title">
${_('Instructions')} ${_('Instructions')}
<a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a> <a class="annotatable-toggle annotatable-toggle-instructions expanded" href="javascript:void(0)">${_('Collapse Instructions')}</a>
</div> </div>
<div class="annotatable-section-body annotatable-instructions"> <div class="annotatable-section-body annotatable-instructions">
${instructions_html} ${instructions_html}
</div> </div>
</div> </div>
% endif % endif
<div class="annotatable-section"> <div class="annotatable-section">
<div class="annotatable-content"> <div class="annotatable-content">
<div id="textHolder">${content_html}</div> <div id="textHolder">${content_html}</div>
<div id="sourceCitation">${_('Source:')} ${source}</div> <div id="sourceCitation">${_('Source:')} ${source}</div>
<div id="catchDIV"> <div id="catchDIV">
<div class="annotationListContainer">${_('You do not have any notes.')}</div> <div class="annotationListContainer">${_('You do not have any notes.')}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
function onClickHideInstructions(){ function onClickHideInstructions(){
//Reset function if there is more than one event handler //Reset function if there is more than one event handler
$(this).off(); $(this).off();
$(this).on('click',onClickHideInstructions); $(this).on('click',onClickHideInstructions);
var hide = $(this).html()=='Collapse Instructions'?true:false, var hide = $(this).html()=='Collapse Instructions'?true:false,
cls, txt,slideMethod; cls, txt,slideMethod;
txt = (hide ? 'Expand' : 'Collapse') + ' Instructions'; txt = (hide ? 'Expand' : 'Collapse') + ' Instructions';
cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']); cls = (hide ? ['expanded', 'collapsed'] : ['collapsed', 'expanded']);
slideMethod = (hide ? 'slideUp' : 'slideDown'); slideMethod = (hide ? 'slideUp' : 'slideDown');
$(this).text(txt).removeClass(cls[0]).addClass(cls[1]); $(this).text(txt).removeClass(cls[0]).addClass(cls[1]);
$(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod](); $(this).parents('.annotatable-section:first').find('.annotatable-instructions')[slideMethod]();
} }
$('.annotatable-toggle-instructions').on('click', onClickHideInstructions); $('.annotatable-toggle-instructions').on('click', onClickHideInstructions);
//Grab uri of the course //Grab uri of the course
var parts = window.location.href.split("/"), var parts = window.location.href.split("/"),
uri = ''; uri = '',
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url courseid;
//Change uri in cms for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
var lms_location = $('.sidebar .preview-button').attr('href'); courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
if (typeof lms_location!='undefined'){ //Change uri in cms
uri = window.location.protocol; var lms_location = $('.sidebar .preview-button').attr('href');
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url if (typeof lms_location!='undefined'){
} courseid = parts[4].split(".").join("/");
var unit_id = $('#sequence-list').find('.active').attr("data-element"); uri = window.location.protocol;
uri += unit_id; for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
var pagination = 100, }
is_staff = !('${user.is_staff}'=='False'), var pagination = 100,
is_staff = !('${user.is_staff}'=='False'),
options = { options = {
optionsAnnotator: { optionsAnnotator: {
permissions:{ permissions:{
...@@ -88,7 +89,7 @@ ...@@ -88,7 +89,7 @@
if (annotation.permissions) { if (annotation.permissions) {
tokens = annotation.permissions[action] || []; tokens = annotation.permissions[action] || [];
if (is_staff){ if (is_staff){
return true; return true;
} }
if (tokens.length === 0) { if (tokens.length === 0) {
return true; return true;
...@@ -114,7 +115,7 @@ ...@@ -114,7 +115,7 @@
}, },
}, },
auth: { auth: {
token: "${token}" tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
}, },
store: { store: {
// The endpoint of the store on your server. // The endpoint of the store on your server.
...@@ -139,14 +140,11 @@ ...@@ -139,14 +140,11 @@
offset:0, offset:0,
uri:uri, uri:uri,
media:'text', media:'text',
userid:'${user.email}', userid:'${user.email}',
} }
}, },
highlightTags:{ highlightTags:{
tag: "${tag}", tag: "${tag}",
},
diacriticMarks:{
diacritics: "${diacritic_marks}"
} }
}, },
optionsVideoJS: {techOrder: ["html5","flash","youtube"]}, optionsVideoJS: {techOrder: ["html5","flash","youtube"]},
...@@ -163,11 +161,12 @@ ...@@ -163,11 +161,12 @@
} }
}, },
}; };
var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/"; var imgURLRoot = "${settings.STATIC_URL}" + "js/vendor/ova/catch/img/";
tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova"; tinyMCE.baseURL = "${settings.STATIC_URL}" + "js/vendor/ova";
//remove old instances //remove old instances
if (Annotator._instances.length !== 0) { if (Annotator._instances.length !== 0) {
$('#textHolder').annotator("destroy"); $('#textHolder').annotator("destroy");
} }
...@@ -175,6 +174,7 @@ ...@@ -175,6 +174,7 @@
//Load the plugin Video/Text Annotation //Load the plugin Video/Text Annotation
var ova = new OpenVideoAnnotation.Annotator($('#textHolder'),options); var ova = new OpenVideoAnnotation.Annotator($('#textHolder'),options);
//Catch //Catch
var annotator = ova.annotator, var annotator = ova.annotator,
catchOptions = { catchOptions = {
...@@ -183,7 +183,7 @@ ...@@ -183,7 +183,7 @@
imageUrlRoot:imgURLRoot, imageUrlRoot:imgURLRoot,
showMediaSelector: false, showMediaSelector: false,
showPublicPrivate: true, showPublicPrivate: true,
userId:'${user.email}', userId:'${user.email}',
pagination:pagination,//Number of Annotations per load in the pagination, pagination:pagination,//Number of Annotations per load in the pagination,
flags:is_staff flags:is_staff
}, },
......
...@@ -49,16 +49,18 @@ ...@@ -49,16 +49,18 @@
//Grab uri of the course //Grab uri of the course
var parts = window.location.href.split("/"), var parts = window.location.href.split("/"),
uri = ''; uri = '',
courseid;
for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url for (var index = 0; index <= 9; index += 1) uri += parts[index]+"/"; //Get the unit url
courseid = parts[4] + "/" + parts[5] + "/" + parts[6];
//Change uri in cms //Change uri in cms
var lms_location = $('.sidebar .preview-button').attr('href'); var lms_location = $('.sidebar .preview-button').attr('href');
if (typeof lms_location!='undefined'){ if (typeof lms_location!='undefined'){
courseid = parts[4].split(".").join("/");
uri = window.location.protocol; uri = window.location.protocol;
for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url for (var index = 0; index <= 9; index += 1) uri += lms_location.split("/")[index]+"/"; //Get the unit url
} }
var unit_id = $('#sequence-list').find('.active').attr("data-element");
uri += unit_id;
var pagination = 100, var pagination = 100,
is_staff = !('${user.is_staff}'=='False'), is_staff = !('${user.is_staff}'=='False'),
options = { options = {
...@@ -117,7 +119,7 @@ ...@@ -117,7 +119,7 @@
}, },
}, },
auth: { auth: {
token: "${token}" tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid
}, },
store: { store: {
// The endpoint of the store on your server. // The endpoint of the store on your server.
...@@ -173,6 +175,8 @@ ...@@ -173,6 +175,8 @@
var ova = new OpenVideoAnnotation.Annotator($('#videoHolder'),options); var ova = new OpenVideoAnnotation.Annotator($('#videoHolder'),options);
ova.annotator.addPlugin('Tags'); ova.annotator.addPlugin('Tags');
//Catch //Catch
var annotator = ova.annotator, var annotator = ova.annotator,
...@@ -182,7 +186,7 @@ ...@@ -182,7 +186,7 @@
imageUrlRoot:imgURLRoot, imageUrlRoot:imgURLRoot,
showMediaSelector: false, showMediaSelector: false,
showPublicPrivate: true, showPublicPrivate: true,
userId:'${user.email}', userId:'${user.email}',
pagination:pagination,//Number of Annotations per load in the pagination, pagination:pagination,//Number of Annotations per load in the pagination,
flags:is_staff flags:is_staff
}, },
......
...@@ -15,6 +15,7 @@ urlpatterns = ('', # nopep8 ...@@ -15,6 +15,7 @@ urlpatterns = ('', # nopep8
url(r'^request_certificate$', 'certificates.views.request_certificate'), url(r'^request_certificate$', 'certificates.views.request_certificate'),
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^token$', 'student.views.token', name="token"),
url(r'^login$', 'student.views.signin_user', name="signin_user"), url(r'^login$', 'student.views.signin_user', name="signin_user"),
url(r'^register$', 'student.views.register_user', name="register_user"), url(r'^register$', 'student.views.register_user', name="register_user"),
...@@ -377,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA ...@@ -377,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA
if settings.FEATURES.get('CLASS_DASHBOARD'): if settings.FEATURES.get('CLASS_DASHBOARD'):
urlpatterns += ( urlpatterns += (
# Json request data for metrics for entire course url(r'^class_dashboard/', include('class_dashboard.urls')),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
'class_dashboard.views.all_problem_grade_distribution', name="all_problem_grade_distribution"),
# Json request data for metrics for particular section
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P<section>\d+)$',
'class_dashboard.views.section_problem_grade_distrib', name="section_problem_grade_distrib"),
# For listing students that opened a sub-section
url(r'^get_students_opened_subsection$',
'class_dashboard.dashboard_data.get_students_opened_subsection', name="get_students_opened_subsection"),
# For listing of students' grade per problem
url(r'^get_students_problem_grades$',
'class_dashboard.dashboard_data.get_students_problem_grades', name="get_students_problem_grades"),
) )
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
......
...@@ -35,7 +35,6 @@ django-method-override==0.1.0 ...@@ -35,7 +35,6 @@ django-method-override==0.1.0
djangorestframework==2.3.5 djangorestframework==2.3.5
django==1.4.12 django==1.4.12
feedparser==5.1.3 feedparser==5.1.3
firebase-token-generator==1.3.2
fs==0.4.0 fs==0.4.0
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
glob2==0.3 glob2==0.3
......
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