From 59e3cae4c9c6f2c95a816a8280b470ec6add9486 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 14 May 2014 13:55:26 -0400 Subject: [PATCH 01/34] Revert pull request #3466 --- cms/envs/common.py | 2 +- .../student/firebase_token_generator.py | 99 ++++++++++++++ .../student/tests/test_token_generator.py | 43 ++++++ common/djangoapps/student/tests/tests.py | 25 +++- common/djangoapps/student/views.py | 24 ++++ common/lib/xmodule/xmodule/annotator_token.py | 32 ----- .../xmodule/tests/test_annotator_token.py | 20 --- .../xmodule/tests/test_textannotation.py | 13 +- .../xmodule/tests/test_videoannotation.py | 98 +++++++++++++- .../xmodule/xmodule/textannotation_module.py | 27 ++-- .../xmodule/xmodule/videoannotation_module.py | 82 ++++++++++-- .../ova/annotator-full-firebase-auth.js | 22 --- lms/djangoapps/notes/views.py | 4 +- lms/envs/common.py | 1 - lms/templates/notes.html | 6 +- lms/templates/textannotation.html | 126 +++++++++--------- lms/templates/videoannotation.html | 14 +- lms/urls.py | 1 + requirements/edx/base.txt | 1 - 19 files changed, 464 insertions(+), 176 deletions(-) create mode 100644 common/djangoapps/student/firebase_token_generator.py create mode 100644 common/djangoapps/student/tests/test_token_generator.py delete mode 100644 common/lib/xmodule/xmodule/annotator_token.py delete mode 100644 common/lib/xmodule/xmodule/tests/test_annotator_token.py delete mode 100644 common/static/js/vendor/ova/annotator-full-firebase-auth.js diff --git a/cms/envs/common.py b/cms/envs/common.py index 76e0f4d50f..f7195d2c54 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -318,7 +318,7 @@ PIPELINE_CSS = { 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.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', }, diff --git a/common/djangoapps/student/firebase_token_generator.py b/common/djangoapps/student/firebase_token_generator.py new file mode 100644 index 0000000000..f84a85277e --- /dev/null +++ b/common/djangoapps/student/firebase_token_generator.py @@ -0,0 +1,99 @@ +''' + 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) diff --git a/common/djangoapps/student/tests/test_token_generator.py b/common/djangoapps/student/tests/test_token_generator.py new file mode 100644 index 0000000000..1eb09c9173 --- /dev/null +++ b/common/djangoapps/student/tests/test_token_generator.py @@ -0,0 +1,43 @@ +""" +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) diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 199a794bc4..c28a54afe8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -26,7 +26,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.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 import shoppingcart @@ -498,3 +498,26 @@ class AnonymousLookupTable(TestCase): anonymous_id = anonymous_id_for_user(self.user, self.course.id) real_user = user_by_anonymous_id(anonymous_id) 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]) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index b199196679..5cecaff5df 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -44,6 +44,7 @@ from student.models import ( create_comments_service_user, PasswordHistory ) from student.forms import PasswordResetFormNoActive +from student.firebase_token_generator import create_token from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow from certificates.models import CertificateStatuses, certificate_status_for_student @@ -1851,3 +1852,26 @@ def change_email_settings(request): track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard') return JsonResponse({"success": True}) + + +@login_required +def token(request): + ''' + 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. + ''' + course_id = request.GET.get("course_id") + course = course_from_id(course_id) + 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) + secret = course.annotation_token_secret + custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": request.user.email, "ttl": 86400} + newtoken = create_token(secret, custom_data) + response = HttpResponse(newtoken, mimetype="text/plain") + return response diff --git a/common/lib/xmodule/xmodule/annotator_token.py b/common/lib/xmodule/xmodule/annotator_token.py deleted file mode 100644 index 6fa5695978..0000000000 --- a/common/lib/xmodule/xmodule/annotator_token.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -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 diff --git a/common/lib/xmodule/xmodule/tests/test_annotator_token.py b/common/lib/xmodule/xmodule/tests/test_annotator_token.py deleted file mode 100644 index ae06808bba..0000000000 --- a/common/lib/xmodule/xmodule/tests/test_annotator_token.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -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 diff --git a/common/lib/xmodule/xmodule/tests/test_textannotation.py b/common/lib/xmodule/xmodule/tests/test_textannotation.py index 907eb78780..397e3990ef 100644 --- a/common/lib/xmodule/xmodule/tests/test_textannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_textannotation.py @@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase): 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): """ Tests to make sure that the instructions are correctly pulled from the sample xml above. @@ -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 """ 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) diff --git a/common/lib/xmodule/xmodule/tests/test_videoannotation.py b/common/lib/xmodule/xmodule/tests/test_videoannotation.py index 4a081803aa..cb63d05503 100644 --- a/common/lib/xmodule/xmodule/tests/test_videoannotation.py +++ b/common/lib/xmodule/xmodule/tests/test_videoannotation.py @@ -34,6 +34,100 @@ class VideoAnnotationModuleTestCase(unittest.TestCase): 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 = 'test' + 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 = 'test' + + 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 = 'test' + + 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('test') + + 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 = 'z' + expected_el = etree.fromstring(expected_html) + + actual_el = etree.fromstring('z') + 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): """ This test ensures that if an instruction exists it is pulled and @@ -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. """ - context = self.mod.get_html() # pylint: disable=W0212 - for key in ['display_name', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'annotation_storage']: + context = self.mod.get_html() + for key in ['display_name', 'content_html', 'instructions_html', 'sourceUrl', 'typeSource', 'poster', 'alert', 'annotation_storage']: self.assertIn(key, context) diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index 4a673eb33e..1d732d8709 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -6,7 +6,6 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String -from xmodule.annotator_token import retrieve_token import textwrap @@ -31,7 +30,7 @@ class AnnotatableFields(object): scope=Scope.settings, default='Text Annotation', ) - instructor_tags = String( + tags = String( 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", scope=Scope.settings, @@ -44,7 +43,6 @@ class AnnotatableFields(object): 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_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): @@ -61,9 +59,15 @@ class TextAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user_email = "" - if self.runtime.get_real_user is not None: - self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + + 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): """ Removes from the xmltree and returns them as a string, otherwise None. """ @@ -78,13 +82,13 @@ class TextAnnotationModule(AnnotatableFields, XModule): """ Renders parameters to template. """ context = { 'display_name': self.display_name_with_default, - 'tag': self.instructor_tags, + 'tag': self.tags, 'source': self.source, 'instructions_html': self.instructions, - 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, - 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'content_html': self._render_content(), + 'annotation_storage': self.annotation_storage_url } + return self.system.render_template('textannotation.html', context) @@ -97,7 +101,6 @@ class TextAnnotationDescriptor(AnnotatableFields, RawDescriptor): def non_editable_metadata_fields(self): non_editable_fields = super(TextAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields.extend([ - TextAnnotationDescriptor.annotation_storage_url, - TextAnnotationDescriptor.annotation_token_secret, + TextAnnotationDescriptor.annotation_storage_url ]) return non_editable_fields diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index 68e5b40413..5f31509d01 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -7,7 +7,6 @@ from pkg_resources import resource_string from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String -from xmodule.annotator_token import retrieve_token import textwrap @@ -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") 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_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): '''Video Annotation Module''' @@ -56,9 +55,73 @@ class VideoAnnotationModule(AnnotatableFields, XModule): self.instructions = self._extract_instructions(xmltree) self.content = etree.tostring(xmltree, encoding='unicode') - self.user_email = "" - if self.runtime.get_real_user is not None: - self.user_email = self.runtime.get_real_user(self.runtime.anonymous_student_id).email + self.highlight_colors = ['yellow', 'orange', 'purple', 'blue', 'green'] + + 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): """ Removes from the xmltree and returns them as a string, otherwise None. """ @@ -91,9 +154,9 @@ class VideoAnnotationModule(AnnotatableFields, XModule): 'sourceUrl': self.sourceurl, 'typeSource': extension, 'poster': self.poster_url, - 'content_html': self.content, - 'annotation_storage': self.annotation_storage_url, - 'token': retrieve_token(self.user_email, self.annotation_token_secret), + 'alert': self, + 'content_html': self._render_content(), + 'annotation_storage': self.annotation_storage_url } return self.system.render_template('videoannotation.html', context) @@ -108,7 +171,6 @@ class VideoAnnotationDescriptor(AnnotatableFields, RawDescriptor): def non_editable_metadata_fields(self): non_editable_fields = super(VideoAnnotationDescriptor, self).non_editable_metadata_fields non_editable_fields.extend([ - VideoAnnotationDescriptor.annotation_storage_url, - VideoAnnotationDescriptor.annotation_token_secret, + VideoAnnotationDescriptor.annotation_storage_url ]) return non_editable_fields diff --git a/common/static/js/vendor/ova/annotator-full-firebase-auth.js b/common/static/js/vendor/ova/annotator-full-firebase-auth.js deleted file mode 100644 index defc25fc95..0000000000 --- a/common/static/js/vendor/ova/annotator-full-firebase-auth.js +++ /dev/null @@ -1,22 +0,0 @@ -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 diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py index 1e14fcaa25..b6670a7e09 100644 --- a/lms/djangoapps/notes/views.py +++ b/lms/djangoapps/notes/views.py @@ -4,7 +4,6 @@ from edxmako.shortcuts import render_to_response from courseware.courses import get_course_with_access from notes.models import Note from notes.utils import notes_enabled_for_course -from xmodule.annotator_token import retrieve_token @login_required @@ -23,8 +22,7 @@ def notes(request, course_id): 'course': course, 'notes': notes, 'student': student, - 'storage': storage, - 'token': retrieve_token(student.email, course.annotation_token_secret), + 'storage': storage } return render_to_response('notes.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0c2b37b2d5..9340aecc54 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -828,7 +828,6 @@ main_vendor_js = [ 'js/vendor/swfobject/swfobject.js', 'js/vendor/jquery.ba-bbq.min.js', 'js/vendor/ova/annotator-full.js', - 'js/vendor/ova/annotator-full-firebase-auth.js', 'js/vendor/ova/video.dev.js', 'js/vendor/ova/vjs.youtube.js', 'js/vendor/ova/rangeslider.js', diff --git a/lms/templates/notes.html b/lms/templates/notes.html index e44a78b08e..d896725581 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -68,8 +68,10 @@ //Grab uri of the course var parts = window.location.href.split("/"), - uri = ''; + uri = '', + courseid; for (var index = 0; index <= 6; index += 1) uri += parts[index]+"/"; //Get the unit url + courseid = parts[4] + "/" + parts[5] + "/" + parts[6]; var pagination = 100, is_staff = false, options = { @@ -128,7 +130,7 @@ }, }, auth: { - token: "${token}" + tokenUrl: location.protocol+'//'+location.host+"/token?course_id="+courseid }, store: { // The endpoint of the store on your server. diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index f69cb7b68c..3532681051 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -1,63 +1,64 @@ <%! from django.utils.translation import ugettext as _ %>
-
- % if display_name is not UNDEFINED and display_name is not None: -
${display_name}
- % endif -
- % if instructions_html is not UNDEFINED and instructions_html is not None: -
-
- ${_('Instructions')} - ${_('Collapse Instructions')} -
-
- ${instructions_html} -
-
- % endif -
-
-
${content_html}
-
${_('Source:')} ${source}
-
-
${_('You do not have any notes.')}
-
-
-
+
+ % if display_name is not UNDEFINED and display_name is not None: +
${display_name}
+ % endif +
+ % if instructions_html is not UNDEFINED and instructions_html is not None: +
+
+ ${_('Instructions')} + ${_('Collapse Instructions')} +
+
+ ${instructions_html} +
+
+ % endif +
+
+
${content_html}
+
${_('Source:')} ${source}
+
+
${_('You do not have any notes.')}
+
+
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 7d4cee28bc..3956a3e15f 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -25,6 +25,13 @@ <%static:css group='style-vendor-tinymce-content'/> <%static:css group='style-vendor-tinymce-skin'/> <%static:css group='style-course'/> + From 6b84f8ab862c13f8448bf96ed046c2b773b01e84 Mon Sep 17 00:00:00 2001 From: Waheed Ahmed Date: Thu, 15 May 2014 17:07:10 +0500 Subject: [PATCH 04/34] Fixed on instructor dash too. --- .../instructor/instructor_dashboard_2/student_admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 93856d6695..7fa5d6eb74 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -10,7 +10,7 @@


- ${_("View Gradebook")} + ${_("View Gradebook")}


%endif From 356723a634e7d1b5f30610d63de40dbf6f0ca3ab Mon Sep 17 00:00:00 2001 From: Alison Hodges Date: Thu, 15 May 2014 09:23:05 -0400 Subject: [PATCH 05/34] Correcting "make html" errors --- .../source/exercises_tools/problem_with_hint.rst | 2 +- .../course_authors/source/exercises_tools/vitalsource.rst | 2 +- .../source/releasing_course/beta_testing.rst | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en_us/course_authors/source/exercises_tools/problem_with_hint.rst b/docs/en_us/course_authors/source/exercises_tools/problem_with_hint.rst index 102ecf2637..999adee40d 100644 --- a/docs/en_us/course_authors/source/exercises_tools/problem_with_hint.rst +++ b/docs/en_us/course_authors/source/exercises_tools/problem_with_hint.rst @@ -57,7 +57,7 @@ To create the above problem: -.. _Drag and Drop Problem XML: +.. _Problem with Adaptive Hint XML: ********************************* Problem with Adaptive Hint XML diff --git a/docs/en_us/course_authors/source/exercises_tools/vitalsource.rst b/docs/en_us/course_authors/source/exercises_tools/vitalsource.rst index 5404b804ea..28dd2cebff 100644 --- a/docs/en_us/course_authors/source/exercises_tools/vitalsource.rst +++ b/docs/en_us/course_authors/source/exercises_tools/vitalsource.rst @@ -10,7 +10,7 @@ The VitalSource Bookshelf e-reader tool provides your students with easy access :width: 500 :alt: VitalSource e-book with highlighted note -For more information about Vital Source and its features, visit the `VitalSource Bookshelf support site `_. +For more information about Vital Source and its features, visit the `VitalSource Bookshelf support site `_. .. 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. diff --git a/docs/en_us/course_authors/source/releasing_course/beta_testing.rst b/docs/en_us/course_authors/source/releasing_course/beta_testing.rst index a21470590d..2390c6180f 100644 --- a/docs/en_us/course_authors/source/releasing_course/beta_testing.rst +++ b/docs/en_us/course_authors/source/releasing_course/beta_testing.rst @@ -194,9 +194,9 @@ When you add beta testers, note the following. .. _Add_Testers_Bulk: --------------------------- +================================ Add Multiple Beta Testers --------------------------- +================================ 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 @@ -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. ------------------------------ +================================ Add Beta Testers Individually ------------------------------ +================================ To add a single beta tester: From 026e6e4a6d6c699e7c85a3d54fbd9c76d5a1c755 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 15 May 2014 11:51:06 -0400 Subject: [PATCH 06/34] Disable failing cms acceptance tests --- .../contentstore/features/subsection.feature | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 7f0d7b85e4..9c4d4cecdb 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -38,13 +38,14 @@ Feature: CMS.Create Subsection 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 - Scenario: Set release and due dates of subsection on enter - Given I have opened a new subsection in Studio - And I set the subsection release date on enter to 04/04/2014 03:00 - And I set the subsection due date on enter to 04/04/2014 04:00 - And I reload the page - 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 +# Disabling due to failure on master. JZ 05/14/2014 TODO: fix +# Scenario: Set release and due dates of subsection on enter +# Given I have opened a new subsection in Studio +# And I set the subsection release date on enter to 04/04/2014 03:00 +# And I set the subsection due date on enter to 04/04/2014 04:00 +# And I reload the page +# 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 Given I have opened a new course section in Studio @@ -55,15 +56,16 @@ Feature: CMS.Create Subsection And I confirm the prompt Then the subsection does not exist - Scenario: Sync to Section - Given I have opened a new course section in Studio - And I click the Edit link for the release date - And I set the section release date to 01/02/2103 - And I have added a new subsection - And I click on the subsection - And I set the subsection release date to 01/20/2103 - And I reload the page - And I click the link to sync release date to section - And I wait for "1" second - And I reload the page - Then I see the subsection release date is 01/02/2103 +# Disabling due to failure on master. JZ 05/14/2014 TODO: fix +# Scenario: Sync to Section +# Given I have opened a new course section in Studio +# And I click the Edit link for the release date +# And I set the section release date to 01/02/2103 +# And I have added a new subsection +# And I click on the subsection +# And I set the subsection release date to 01/20/2103 +# And I reload the page +# And I click the link to sync release date to section +# And I wait for "1" second +# And I reload the page +# Then I see the subsection release date is 01/02/2103 From 529d2fa33faccc9f19146daad7e094a01b14e2c9 Mon Sep 17 00:00:00 2001 From: Mark Hoeber Date: Tue, 13 May 2014 13:54:23 -0400 Subject: [PATCH 07/34] Release Notes file setup DOC-400 --- .../en_us/release_notes/source/05-15-2014.rst | 110 ++++++++++++++++++ docs/en_us/release_notes/source/index.rst | 1 + docs/en_us/release_notes/source/links.rst | 11 +- 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 docs/en_us/release_notes/source/05-15-2014.rst diff --git a/docs/en_us/release_notes/source/05-15-2014.rst b/docs/en_us/release_notes/source/05-15-2014.rst new file mode 100644 index 0000000000..6ce8b975e2 --- /dev/null +++ b/docs/en_us/release_notes/source/05-15-2014.rst @@ -0,0 +1,110 @@ +################################### +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 diff --git a/docs/en_us/release_notes/source/index.rst b/docs/en_us/release_notes/source/index.rst index 471b78945f..f00cde5a41 100755 --- a/docs/en_us/release_notes/source/index.rst +++ b/docs/en_us/release_notes/source/index.rst @@ -19,6 +19,7 @@ There is a page in this document for each update to the edX system on `edx.org`_ :maxdepth: 1 read_me + 05-15-2014 05-12-2014 04-29-2014 04-23-2014 diff --git a/docs/en_us/release_notes/source/links.rst b/docs/en_us/release_notes/source/links.rst index 67ceb17941..82ab96e16c 100644 --- a/docs/en_us/release_notes/source/links.rst +++ b/docs/en_us/release_notes/source/links.rst @@ -150,6 +150,13 @@ .. _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 .. _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 @@ .. _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/ \ No newline at end of file +.. _edX XBlock Documentation: http://edx.readthedocs.org/projects/xblock/en/latest/ + +.. _Analytics: http://edx.readthedocs.org/projects/userdocs/en/latest/analytics.html \ No newline at end of file From 9bc7a518ee22b5bbf11ff239d771ac6739f245ec Mon Sep 17 00:00:00 2001 From: David Adams Date: Fri, 18 Apr 2014 12:16:10 -0700 Subject: [PATCH 08/34] Fixes issue with metrics tab click handlers Click handlers were not getting attached to DOM elements in some cases on slow running machines. Added logic to attach handlers when elements are ready. Added 2 buttons on metrics tab: Download Subsection Data for downloading to csv. Download Problem Data for downloading to csv. --- .../class_dashboard/dashboard_data.py | 137 +++++++--- .../tests/test_dashboard_data.py | 60 +++- lms/djangoapps/class_dashboard/urls.py | 30 ++ .../instructor/views/instructor_dashboard.py | 2 + .../sass/course/instructor/_instructor_2.scss | 22 +- .../class_dashboard/all_section_metrics.js | 21 +- .../class_dashboard/d3_stacked_bar_graph.js | 14 +- .../courseware/instructor_dashboard.html | 4 +- .../instructor_dashboard_2/metrics.html | 257 ++++++++++++------ lms/urls.py | 18 +- 10 files changed, 411 insertions(+), 154 deletions(-) create mode 100644 lms/djangoapps/class_dashboard/urls.py diff --git a/lms/djangoapps/class_dashboard/dashboard_data.py b/lms/djangoapps/class_dashboard/dashboard_data.py index 209d647faf..aa7eb206ba 100644 --- a/lms/djangoapps/class_dashboard/dashboard_data.py +++ b/lms/djangoapps/class_dashboard/dashboard_data.py @@ -2,6 +2,7 @@ Computes the data to display on the Instructor Dashboard """ from util.json_request import JsonResponse +import json from courseware import models from django.db.models import Count @@ -21,9 +22,12 @@ def get_problem_grade_distribution(course_id): `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 '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 @@ -34,6 +38,7 @@ def get_problem_grade_distribution(course_id): ).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade')) prob_grade_distrib = {} + total_student_count = {} # Loop through resultset building data for each problem for row in db_query: @@ -53,7 +58,10 @@ def get_problem_grade_distribution(course_id): '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): @@ -136,7 +144,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 """ - prob_grade_distrib = get_problem_grade_distribution(course_id) + prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id) d3_data = [] # Retrieve course object down to problems @@ -178,19 +186,24 @@ def get_d3_problem_grade_distrib(course_id): for (grade, count_grade) in problem_info['grade_distrib']: percent = 0.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 - tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( - label=label, - problem_name=problem_name, - count_grade=count_grade, - students=_("students"), - percent=percent, - grade=grade, - max_grade=max_grade, - questions=_("questions"), - ) + # Compute percent of students with this grade + student_count_percent = 0 + if total_student_count.get(child.location.url(), 0) > 0: + student_count_percent = count_grade * 100 / total_student_count[child.location.url()] + + # Tooltip parameters for problem in grade distribution view + tooltip = { + 'type': 'problem', + 'label': label, + '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 stack_data.append({ @@ -246,11 +259,14 @@ def get_d3_sequential_open_distrib(course_id): num_students = sequential_open_distrib[subsection.location.url()] stack_data = [] - tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format( - num_students=num_students, - subsection_num=c_subsection, - subsection_name=subsection_name, - ) + + # Tooltip parameters for subsection in open_distribution view + tooltip = { + 'type': 'subsection', + 'num_students': num_students, + 'subsection_num': c_subsection, + 'subsection_name': subsection_name + } stack_data.append({ 'color': 0, @@ -329,19 +345,18 @@ def get_d3_section_grade_distrib(course_id, section): for (grade, count_grade) in grade_distrib[problem]['grade_distrib']: percent = 0.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 - tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format( - problem_info_x=problem_info[problem]['x_value'], - count_grade=count_grade, - students=_("students"), - percent=percent, - problem_info_n=problem_info[problem]['display_name'], - grade=grade, - max_grade=max_grade, - questions=_("questions"), - ) + tooltip = { + 'type': 'problem', + 'problem_info_x': problem_info[problem]['x_value'], + 'count_grade': count_grade, + 'percent': percent, + 'problem_info_n': problem_info[problem]['display_name'], + 'grade': grade, + 'max_grade': max_grade, + } stack_data.append({ 'color': percent, @@ -415,6 +430,7 @@ def get_students_opened_subsection(request, csv=False): If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames for CSV download. """ + module_id = request.GET.get('module_id') csv = request.GET.get('csv') @@ -447,9 +463,11 @@ def get_students_opened_subsection(request, csv=False): return JsonResponse(response_payload) else: 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: results.append([student['student__profile__name'], student['student__username']]) @@ -507,7 +525,7 @@ def get_students_problem_grades(request, csv=False): tooltip = request.GET.get('tooltip') 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: percent = 0 @@ -519,11 +537,60 @@ def get_students_problem_grades(request, csv=False): 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): """ Utility function """ filename = filename.replace(" ", "_") - filename = filename.encode('ascii') + filename = filename.encode('utf-8') filename = filename[0:25] + '.csv' return filename diff --git a/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py index a011ee6dce..5d20a8fa3c 100644 --- a/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py +++ b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py @@ -95,12 +95,15 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): 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: max_grade = prob_grade_distrib[problem]['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): sequential_open_distrib = get_sequential_open_distrib(self.course.id) @@ -243,6 +246,61 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): # Check response contains 1 line for each user +1 for the header 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): section_display_name = get_section_display_name(self.course.id) diff --git a/lms/djangoapps/class_dashboard/urls.py b/lms/djangoapps/class_dashboard/urls.py new file mode 100644 index 0000000000..24198260e3 --- /dev/null +++ b/lms/djangoapps/class_dashboard/urls.py @@ -0,0 +1,30 @@ +""" +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[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$', + 'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"), + + url(r'^(?P[^/]+/[^/]+/[^/]+)/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[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P
\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"), +) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index a7c7bee453..9b0eeab55b 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -250,10 +250,12 @@ def _section_metrics(course_id, access): 'section_key': 'metrics', 'section_display_name': ('Metrics'), 'access': access, + 'course_id': course_id, 'sub_section_display_name': get_section_display_name(course_id), 'section_has_problem': get_array_section_has_problem(course_id), 'get_students_opened_subsection_url': reverse('get_students_opened_subsection'), 'get_students_problem_grades_url': reverse('get_students_problem_grades'), + 'post_metrics_data_csv_url': reverse('post_metrics_data_csv'), } return section_data diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 35a984c6e8..96959814f0 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -591,17 +591,16 @@ section.instructor-dashboard-content-2 { .instructor-dashboard-wrapper-2 section.idash-section#metrics { - .metrics-container { + .metrics-container, .metrics-header-container { position: relative; width: 100%; float: left; clear: both; margin-top: 25px; - - .metrics-left { + + .metrics-left, .metrics-left-header { position: relative; width: 30%; - height: 640px; float: left; margin-right: 2.5%; @@ -609,10 +608,13 @@ section.instructor-dashboard-content-2 { width: 100%; } } - .metrics-right { + .metrics-section.metrics-left { + height: 640px; + } + + .metrics-right, .metrics-right-header { position: relative; width: 65%; - height: 295px; float: left; margin-left: 2.5%; margin-bottom: 25px; @@ -622,6 +624,10 @@ section.instructor-dashboard-content-2 { } } + .metrics-section.metrics-right { + height: 295px; + } + svg { .stacked-bar { cursor: pointer; @@ -718,10 +724,6 @@ section.instructor-dashboard-content-2 { border-radius: 5px; margin-top: 25px; } - - input#graph_reload { - display: none; - } } } diff --git a/lms/templates/class_dashboard/all_section_metrics.js b/lms/templates/class_dashboard/all_section_metrics.js index fc417255c7..86651eaaf4 100644 --- a/lms/templates/class_dashboard/all_section_metrics.js +++ b/lms/templates/class_dashboard/all_section_metrics.js @@ -1,4 +1,4 @@ -<%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 from django.core.urlresolvers import reverse @@ -30,6 +30,13 @@ $(function () { 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"), d3.select("#${id_tooltip_prefix}"+i)); barGraphOpened.scale.stackColor.range(["#555555","#555555"]); @@ -68,6 +75,17 @@ $(function () { 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"), d3.select("#${id_tooltip_prefix}"+i)); barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]); @@ -83,6 +101,7 @@ $(function () { i+=1; } + }); }); \ No newline at end of file diff --git a/lms/templates/class_dashboard/d3_stacked_bar_graph.js b/lms/templates/class_dashboard/d3_stacked_bar_graph.js index 8552b3f48e..fd1bdb0f33 100644 --- a/lms/templates/class_dashboard/d3_stacked_bar_graph.js +++ b/lms/templates/class_dashboard/d3_stacked_bar_graph.js @@ -349,8 +349,20 @@ edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) { var top = pos[1]-10; 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") - .text(d.tooltip); + .text(tooltip_str); if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width()) left -= (width+30); diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 00f12011e8..9bf61b34f1 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -725,7 +725,9 @@ function goto( mode) %endfor %endif diff --git a/lms/templates/instructor/instructor_dashboard_2/metrics.html b/lms/templates/instructor/instructor_dashboard_2/metrics.html index fbd07e5a19..535750ba15 100644 --- a/lms/templates/instructor/instructor_dashboard_2/metrics.html +++ b/lms/templates/instructor/instructor_dashboard_2/metrics.html @@ -1,4 +1,4 @@ -<%! from django.utils.translation import ugettext as _ %> + <%! from django.utils.translation import ugettext as _ %> <%page args="section_data"/> @@ -11,19 +11,35 @@ %else: <%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"/> - -

${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}

- +
+

${_("Use Reload Graphs to refresh the graphs.")}

+

+
+
+
+

${_("Subsection Data")}

+

${_("Each bar shows the number of students that opened the subsection.")}

+

${_("You can click on any of the bars to list the students that opened the subsection.")}

+

${_("You can also download this data as a CSV file.")}

+

+
+
+

${_("Grade Distribution Data")}

+

${_("Each bar shows the grade distribution for that problem.")}

+

${_("You can click on any of the bars to list the students that attempted the problem, along with the grades they received.")}

+

${_("You can also download this data as a CSV file.")}

+

+
+
%for i in range(0, len(section_data['sub_section_display_name'])):
-

${_("Section:")} ${section_data['sub_section_display_name'][i]}

+

${_("Section")}: ${section_data['sub_section_display_name'][i]}

-

${_("Count of Students Opened a Subsection")}

${_("Grade Distribution per Problem")}

@@ -46,11 +62,91 @@ diff --git a/lms/urls.py b/lms/urls.py index 96fab1b9d9..9a001dc490 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -378,23 +378,7 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGA if settings.FEATURES.get('CLASS_DASHBOARD'): urlpatterns += ( - # Json request data for metrics for entire course - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$', - 'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/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[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P
\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"), + url(r'^class_dashboard/', include('class_dashboard.urls')), ) if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): From 52bc2aa07076ae64ca143b072c9db5ff83de2da2 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 15 May 2014 16:35:05 -0400 Subject: [PATCH 09/34] Added rst version of cognitive load page --- .../developers/source/cognitive_load.rst | 179 ++++++++++++++++++ docs/en_us/developers/source/index.rst | 1 + 2 files changed, 180 insertions(+) create mode 100644 docs/en_us/developers/source/cognitive_load.rst diff --git a/docs/en_us/developers/source/cognitive_load.rst b/docs/en_us/developers/source/cognitive_load.rst new file mode 100644 index 0000000000..dc05304fed --- /dev/null +++ b/docs/en_us/developers/source/cognitive_load.rst @@ -0,0 +1,179 @@ +************** +Cognitive Load +************** + +This is a checklist of all of the things that we expect a developer to consider +as they are building new or modifying existing functionality. + +Operational Impact +================== + +* Are there new points in the system that require operational monitoring? + + * External system that you now depend on (Mathworks, SoftwareSecure, + CyberSource, etc...) + * New reliance on disk space? + * New stand process (workers? elastic search?) that need to always be available? + * A new queue that needs to be monitored for dequeueing + * Bulk Email --> Amazon SES, Inbound queues, etc... + * Are important feature metrics sent to datadog and is there a + dashboard to monitor them? + +* Am I building a feature that will have impact to the performance of the system? + + * Deep Search + * Grade Downloads + +* Are reasonable log messages being written out for debugging purposes? +* Will this new feature easily startup in the Vagrant image? +* Do we have documentation for how to startup this feature if it has any + new startup requirements? +* Are there any special directories/file system permissions that need to be set? +* Will this have any impact to the CDN related technologies? +* Are we pushing any extra manual burden on the Operations team to have to + provision anything new when new courses launch? when new schools start? etc.... +* Has the feature been tested using a production configuration with vagrant? + +See also: :doc:`deploy-new-service` + +Documentation/Training/Support +============================== + +* Is there appropriate documentation in the context of the product for + this feature? If not, how can we get it to folks? + + * For Studio much of the documentation is in the product. + +* Is this feature big enough that we need to have a session with stake holders + to introduce this feature BEFORE we release it? (PMs, Support, etc...) + + * Paid Certificates + +* Do I have to give some more information to the Escalation Team + so that this can be supported? +* Did you add an entry to CHANGELOG? +* Did you write/edit docstrings for all of your modules, classes, and functions? + +Development +=========== + +* Did you consider a reasonable upgrade path? +* Is this a feature that we need to slowly roll out to different audiences? + + * Bulk Email + +* Have you considered exposing an appropriate amount of configuration options + in case something happens? +* Have you considered a simple way to "disable" this feature if something is broken? + + * Centralized Logging + +* Will this feature require any security provisioning? + + * Which roles use this feature? Does it make sense to ensure that only those + roles can see this feature? + * Assets in the Studio Library + +* Did you ensure that any new libraries are added to appropriate provisioning + scripts and have been checked by OSCM for license appropriateness? +* Is there an open source alternative? +* Are we locked down to any proprietary technologies? (AWS, ...) +* Did you consider making APIs so that others can change the implementation if applicable? +* Did you consider Internationalization (I18N) and Localization (L10N)? +* Did you consider Accessibility (A11y)? +* Will your code work properly in workers? +* Have you considered the large-scale modularity of the code? For example, + xmodule and xblock should not use Django features directly. + +Testing +======= + +* Did you make sure that you tried boundary conditions? +* Did you try unicode input/data? + + * The name of the person in paid certifactes + * The name of the person in bulk email + * The body of the text in bulk email + * etc + +* Did you try funny characters in the input/data? (~!@#$%^&*()';/.,<>, etc...) +* Have you done performance testing on this feature? Do you know how much + performance is good enough? +* Did you ensure that your functionality works across all supported browsers? +* Do you have the right hooks in your HTML to ensure that the views are automatable? +* Are you ready if this feature has 10x the expected usage? +* What happens if an external service does not respond or responds with + a significant delay? +* What are possible failure modes? Do your unit tests exercise these code paths? +* Does this change affect templates and/or JavaScript? If so, are there + Selenium tests for the affected page(s)? Have you tested the affected + page(s) in a sandbox? + +Analytics +========= + +* Are learning analytics events being recorded in an appropriate way? + + * Do your events use a descriptive and uniquely enough event type and + namespace? + * Did you ensure that you capture enough information for the researchers + to benefit from this event information? + * Is it possible to reconstruct the state of your module from the history + of its events? + * Has this new event been documented so that folks downstream know how + to interpret it? + * Are you increasing the amount of logging in any major way? + +* Are you sending appropriate/enough information to MixPanel, + Google Analytics, Segment IO? + +Collaboration +============= +* Are there are other teams that would benefit from knowing about this feature? + + * Forums/LMS - email + +* Does this feature require a special broadcast to external teams as well? + (Stanford, Google, Berkley, etc...) + +Open Source +=========== +* Can we get help from the community on this feature? +* Does the community know enough about this? + +UX/Design/Front End Development +=============================== +* Did you make sure that the feature is going to pass + Accessibility requirements (still TBD)? +* Did you make sure any system/instructional text is I18N ready? +* Did you ensure that basic functionality works across all supported browsers? +* Did you plan for the feature's UI to degrade gracefully (or be + progressively enhanced) based on browser capability? +* Did you review the page/view under all browser/agent conditions - + viewport sizes, images off, css off? +* Did you write any HTML with ideal page/view semantics in mind? +* When writing HTML, did you adhere to standards/conventions around class/id names? +* When writing Sass, did you follow OOCSS/SMACSS philosophy ([1]_, [2]_, [3]_), + variable/extend organization and naming conventions, and UI abstraction conventions? +* When writing Sass, did you document any new variables, + extend-based classes, or mixins? +* When writing/adding JavaScript, did you consider the asset pipeline + and page load timeline? +* When writing JavaScript, did you note what code is for prototyping vs. production? +* When adding new templates, views, assets (Sass, images, plugins/libraries), + did you follow existing naming and file architecture conventions? +* When adding new templates, views, assets (Sass, images, plugins/libraries), + did you add any needed documentation? +* Did you use templates and good Sass architecture to keep DRY? +* Did we document any aspects about the feature (flow, purpose, intent) + that we or other teams will need to know going forward? + +.. [1] http://smacss.com/ +.. [2] http://thesassway.com/intermediate/avoid-nested-selectors-for-more-modular-css +.. [3] http://ianstormtaylor.com/oocss-plus-sass-is-the-best-way-to-css/ + +edX.org Specific +================ + +* Ensure that you have not broken import/export? +* Ensure that you have not broken video player? (Lyla video) diff --git a/docs/en_us/developers/source/index.rst b/docs/en_us/developers/source/index.rst index a2ef6f6b23..ef16acbc89 100644 --- a/docs/en_us/developers/source/index.rst +++ b/docs/en_us/developers/source/index.rst @@ -23,6 +23,7 @@ Contents: analytics.rst process/index testing/index + cognitive_load APIs ----- From 138bd78e934a8e099985399fc99e36fcee8e6564 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 15 May 2014 16:44:53 -0400 Subject: [PATCH 10/34] add So You Want to Deploy a New Service --- .../developers/source/deploy-new-service.rst | 133 ++++++++++++++++++ docs/en_us/developers/source/index.rst | 1 + 2 files changed, 134 insertions(+) create mode 100644 docs/en_us/developers/source/deploy-new-service.rst diff --git a/docs/en_us/developers/source/deploy-new-service.rst b/docs/en_us/developers/source/deploy-new-service.rst new file mode 100644 index 0000000000..c63ac6ab62 --- /dev/null +++ b/docs/en_us/developers/source/deploy-new-service.rst @@ -0,0 +1,133 @@ +*********************************** +So You Want to Deploy a New Service +*********************************** + +Intro +===== + +This page is a work-in-progress aimed at capturing all the details needed to +deploy a new service in the edX environment. + +Considerations +============== + +What Does Your Service Do +------------------------- +Understanding how your service works and what it does helps Ops support +the service in production. + +Sizing and Resource Profile +--------------------------- +What class of machine does your service require. What resources are most +likely to be bottlenecks for your service, CPU, memory, bandwidth, something else? + +Customers +--------- +Who will be consuming your service? What is the anticipated initial usage? +What factors will cause usage to grow? How many users can your service support? + +Code +---- +What repository or repositories does your service require. +Will your service be deployed from a non-public repo? + +Ideally your service should follow the same release management process as the LMS. +This is documented in the wiki, so please ensure you understand that process in depth. + +Was the service code reviewed? + +Settings +-------- +How does your service read in environment specific settings? Were all +hard-coded references to values that should be settings, e.g., database URLs +and credentials, message queue endpoints, etc., found and resolved during +code review? + +License +------- +Is the license included in the repo? + +How does your service run +------------------------- +Is it HTTP based? Does it run periodically? Both? + +Persistence +----------- +Ops will need to know the following things: + +* What persistence needs does you service have + + * Will it connect to an existing database? + * Will it connect to Mongo + +* What are the least permissive permissions your service needs to do its job. + +Logging +------- + +It's important that your application logging in built out to provide sufficient +feedback for problem determination as well as ensuring that it is operating as +desired. It's also important that your service log using our deployment +standards, i.e., logs vi syslog in deployment environments and utilizes the +standard log format for syslog. Can the logs be consumed by Splunk? They +should not be if they contain data discussed in the Data Security section below. + +Metrics +------- +What are the key metrics for your application? Concurrent users? +Transactions per second? Ideally you should create a DataDog view that +captures the key metrics for your service and provided an instant gauge of +overally service health. + +Messaging +--------- +Does your service need to access a message queue. + +Email +----- +Does your service need to send email + +Access to Other Service +----------------------- +Does your service need access to other service either within or +outside of the edX environment. Some example might be, the comment service, +the LMS, YouTube, s3 buckets, etc. + +Service Monitoring +------------------ +Your service should have a facility for remote monitoring that has the +following characteristics: + +* It should exercise all the components that your service requires to run successfully. +* It should be necessary and sufficient for ensuring your service is healthy. +* It should be secure. +* It should not open your service to DDOS attacks. + +Fault Tolerance and Scalability +------------------------------- +How can your application be deployed to ensure that it is fault tolerant +and scalable? + +Network Access +-------------- +From where should your service be accessible. + +Data Security +------------- +Will your application be storing or handling data in any of the +following categories: + +* Personally Identifiable Information in General, e.g., user's email addresses. +* Tracking log data +* edX confidential data + +Testing +------- +Has your service been load tested? What there the details of the test. +What determinations can we make regarding when we will need to scale if usage +trend upward? How can ops exercise your service in order to tests end-to-end +integration. We love no-op-able tasks. + +Additional Requirements +----------------------- +Anything else we should know about. diff --git a/docs/en_us/developers/source/index.rst b/docs/en_us/developers/source/index.rst index ef16acbc89..23a6e5d26d 100644 --- a/docs/en_us/developers/source/index.rst +++ b/docs/en_us/developers/source/index.rst @@ -24,6 +24,7 @@ Contents: process/index testing/index cognitive_load + deploy-new-service APIs ----- From 74aa3a5ba00fc971897ee9ed832552dafa995483 Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Thu, 8 May 2014 14:28:20 +0500 Subject: [PATCH 11/34] create course gorups properly on course import STUD-1595 --- cms/djangoapps/contentstore/tests/utils.py | 11 ++-- .../contentstore/views/import_export.py | 35 +++++------- .../views/tests/test_import_export.py | 55 +++++++++++++++++-- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 9c2e46ab82..92910189bf 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -4,16 +4,16 @@ Utilities for contentstore tests import json -from student.models import Registration from django.contrib.auth.models import User from django.test.client import Client 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.factories import CourseFactory, ItemFactory from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.utils import get_modulestore -from xmodule.modulestore.django import loc_mapper +from student.models import Registration def parse_json(response): @@ -98,9 +98,9 @@ class CourseTestCase(ModuleStoreTestCase): ) 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' password = 'foo' @@ -113,7 +113,8 @@ class CourseTestCase(ModuleStoreTestCase): nonstaff.save() client = Client() - client.login(username=uname, password=password) + if authenticate: + client.login(username=uname, password=password) return client, nonstaff def populate_course(self): diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index f67810e83c..fb10031802 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -4,38 +4,35 @@ courses """ import logging import os -import tarfile -import shutil import re -from tempfile import mkdtemp +import shutil +import tarfile from path import path +from tempfile import mkdtemp from django.conf import settings -from django.http import HttpResponse from django.contrib.auth.decorators import login_required -from django_future.csrf import ensure_csrf_cookie -from django.core.servers.basehttp import FileWrapper -from django.core.files.temp import NamedTemporaryFile from django.core.exceptions import SuspiciousOperation, PermissionDenied -from django.http import HttpResponseNotFound -from django.views.decorators.http import require_http_methods, require_GET +from django.core.files.temp import NamedTemporaryFile +from django.core.servers.basehttp import FileWrapper +from django.http import HttpResponse, HttpResponseNotFound 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 xmodule.modulestore.xml_importer import import_from_xml from xmodule.contentstore.django import contentstore -from xmodule.modulestore.xml_exporter import export_to_xml -from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.exceptions import SerializationError - +from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.modulestore.locator import BlockUsageLocator -from .access import has_course_access +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml_exporter import export_to_xml -from util.json_request import JsonResponse +from .access import has_course_access from extract_tar import safetar_extractall -from student.roles import CourseInstructorRole, CourseStaffRole from student import auth +from student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff +from util.json_request import JsonResponse __all__ = ['import_handler', 'import_status_handler', 'export_handler'] @@ -232,10 +229,6 @@ def import_handler(request, tag=None, package_id=None, branch=None, version_guid session_status[key] = 3 request.session.modified = True - auth.add_users(request.user, CourseInstructorRole(new_location), request.user) - auth.add_users(request.user, CourseStaffRole(new_location), request.user) - logging.debug('created all course groups at {0}'.format(new_location)) - # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=W0703 log.exception( diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index e01e6ba565..dc0a3cb9c4 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -1,25 +1,28 @@ """ Unit tests for course import and export """ +import copy +import json +import logging import os import shutil import tarfile import tempfile -import copy from path import path -import json -import logging -from uuid import uuid4 from pymongo import MongoClient +from uuid import uuid4 -from contentstore.tests.utils import CourseTestCase from django.test.utils import override_settings from django.conf import settings -from xmodule.modulestore.django import loc_mapper from xmodule.contentstore.django import _CONTENTSTORE +from xmodule.modulestore.django import loc_mapper 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['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex @@ -107,6 +110,46 @@ class ImportTestCase(CourseTestCase): 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 ##################################################### # Each of these methods creates a tarfile with a single type of unsafe # content. From 7f5c810ac8c0e3b5dbc731154e0eeb30aecdbe76 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Fri, 16 May 2014 09:00:12 -0400 Subject: [PATCH 12/34] Skip language changing tests --- common/test/acceptance/tests/test_lms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/test_lms.py index 75ccf85036..008525215b 100644 --- a/common/test/acceptance/tests/test_lms.py +++ b/common/test/acceptance/tests/test_lms.py @@ -64,7 +64,7 @@ class RegistrationTest(UniqueCourseTest): course_names = dashboard.available_courses self.assertIn(self.course_info['display_name'], course_names) - +@skip("TE-399") class LanguageTest(UniqueCourseTest): """ Tests that the change language functionality on the dashboard works From adf6df224edff01f17a595ae08edecdb6031eb5a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 16 May 2014 09:28:05 -0400 Subject: [PATCH 13/34] address review comments from @nedbat --- docs/en_us/developers/source/cognitive_load.rst | 16 +++++++++------- .../developers/source/deploy-new-service.rst | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/en_us/developers/source/cognitive_load.rst b/docs/en_us/developers/source/cognitive_load.rst index dc05304fed..e37e087881 100644 --- a/docs/en_us/developers/source/cognitive_load.rst +++ b/docs/en_us/developers/source/cognitive_load.rst @@ -1,6 +1,6 @@ -************** -Cognitive Load -************** +******************* +Code Considerations +******************* This is a checklist of all of the things that we expect a developer to consider as they are building new or modifying existing functionality. @@ -20,13 +20,16 @@ Operational Impact dashboard to monitor them? * Am I building a feature that will have impact to the performance of the system? + Keep in mind that Open edX needs to support hundreds of thousands if not + millions of students, so be careful that you code will work well when the + numbers get large. * Deep Search * Grade Downloads * Are reasonable log messages being written out for debugging purposes? -* Will this new feature easily startup in the Vagrant image? -* Do we have documentation for how to startup this feature if it has any +* Will this new feature easily start up in the Vagrant image? +* Do we have documentation for how to start up this feature if it has any new startup requirements? * Are there any special directories/file system permissions that need to be set? * Will this have any impact to the CDN related technologies? @@ -44,7 +47,7 @@ Documentation/Training/Support * For Studio much of the documentation is in the product. -* Is this feature big enough that we need to have a session with stake holders +* Is this feature big enough that we need to have a session with stakeholders to introduce this feature BEFORE we release it? (PMs, Support, etc...) * Paid Certificates @@ -134,7 +137,6 @@ Collaboration * Forums/LMS - email * Does this feature require a special broadcast to external teams as well? - (Stanford, Google, Berkley, etc...) Open Source =========== diff --git a/docs/en_us/developers/source/deploy-new-service.rst b/docs/en_us/developers/source/deploy-new-service.rst index c63ac6ab62..7cab9d0cd3 100644 --- a/docs/en_us/developers/source/deploy-new-service.rst +++ b/docs/en_us/developers/source/deploy-new-service.rst @@ -68,7 +68,7 @@ Logging It's important that your application logging in built out to provide sufficient feedback for problem determination as well as ensuring that it is operating as desired. It's also important that your service log using our deployment -standards, i.e., logs vi syslog in deployment environments and utilizes the +standards, i.e., logs vs syslog in deployment environments and utilizes the standard log format for syslog. Can the logs be consumed by Splunk? They should not be if they contain data discussed in the Data Security section below. From 867ea2b6374def11547c8e76234c3ba26f126c18 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 16 May 2014 09:28:50 -0400 Subject: [PATCH 14/34] renamed cognitive_load.rst to code-considerations.rst --- .../source/{cognitive_load.rst => code-considerations.rst} | 0 docs/en_us/developers/source/index.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/en_us/developers/source/{cognitive_load.rst => code-considerations.rst} (100%) diff --git a/docs/en_us/developers/source/cognitive_load.rst b/docs/en_us/developers/source/code-considerations.rst similarity index 100% rename from docs/en_us/developers/source/cognitive_load.rst rename to docs/en_us/developers/source/code-considerations.rst diff --git a/docs/en_us/developers/source/index.rst b/docs/en_us/developers/source/index.rst index 23a6e5d26d..ea852221a3 100644 --- a/docs/en_us/developers/source/index.rst +++ b/docs/en_us/developers/source/index.rst @@ -23,7 +23,7 @@ Contents: analytics.rst process/index testing/index - cognitive_load + code-considerations deploy-new-service APIs From 0533859366d581fc9fe84262643fe438ca7f40c0 Mon Sep 17 00:00:00 2001 From: Christine Lytwynec Date: Fri, 16 May 2014 11:17:26 -0400 Subject: [PATCH 15/34] disabled test_lms.py:XBlockAcidNoChildTest.test_acid_block --- common/test/acceptance/tests/test_lms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/test/acceptance/tests/test_lms.py b/common/test/acceptance/tests/test_lms.py index 75ccf85036..c97d62694e 100644 --- a/common/test/acceptance/tests/test_lms.py +++ b/common/test/acceptance/tests/test_lms.py @@ -381,6 +381,10 @@ class XBlockAcidNoChildTest(XBlockAcidBase): ) ).install() + @skip('Flakey test, TE-401') + def test_acid_block(self): + super(XBlockAcidNoChildTest, self).test_acid_block() + class XBlockAcidChildTest(XBlockAcidBase): """ From 14f3bcfe66bc5f3f002c7bfea5e6fbf0ba029f14 Mon Sep 17 00:00:00 2001 From: Sylvia Pearce Date: Thu, 15 May 2014 11:08:03 -0400 Subject: [PATCH 16/34] Update Video docs to reflect new UI --- .../source/Images/VideoComponentEditor.png | Bin 41959 -> 20981 bytes .../source/Images/Video_DownTrans_other.png | Bin 0 -> 4731 bytes .../Images/Video_DownTrans_srt-handout.png | Bin 0 -> 6545 bytes .../source/Images/Video_DownTrans_srt-txt.png | Bin 0 -> 8044 bytes .../course_authors/source/change_log.rst | 4 +- .../source/creating_content/create_video.rst | 162 +++++++++--------- 6 files changed, 82 insertions(+), 84 deletions(-) create mode 100644 docs/en_us/course_authors/source/Images/Video_DownTrans_other.png create mode 100644 docs/en_us/course_authors/source/Images/Video_DownTrans_srt-handout.png create mode 100644 docs/en_us/course_authors/source/Images/Video_DownTrans_srt-txt.png diff --git a/docs/en_us/course_authors/source/Images/VideoComponentEditor.png b/docs/en_us/course_authors/source/Images/VideoComponentEditor.png index 6ea9c3f408e5bd5d3fa93bf4009e3f9d6af1caa6..23bb30e6c61930c161d4b4aaee32a16bb505a124 100644 GIT binary patch literal 20981 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 ZZzONQmCBuD^qLW)?Fq-@lw*Fk{s9RyKlK0r literal 41959 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 diff --git a/docs/en_us/course_authors/source/Images/Video_DownTrans_other.png b/docs/en_us/course_authors/source/Images/Video_DownTrans_other.png new file mode 100644 index 0000000000000000000000000000000000000000..3f7f80f8deb8d517b1b8f5e662c84fc4f4bd75b1 GIT binary patch literal 4731 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 literal 0 HcmV?d00001 diff --git a/docs/en_us/course_authors/source/Images/Video_DownTrans_srt-handout.png b/docs/en_us/course_authors/source/Images/Video_DownTrans_srt-handout.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5e93fa6a3e627c123a2ffae01a0cae554b4133 GIT binary patch literal 6545 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 literal 0 HcmV?d00001 diff --git a/docs/en_us/course_authors/source/change_log.rst b/docs/en_us/course_authors/source/change_log.rst index 09b20f7181..99aa47f4b4 100644 --- a/docs/en_us/course_authors/source/change_log.rst +++ b/docs/en_us/course_authors/source/change_log.rst @@ -12,10 +12,12 @@ May, 2014 * - Date - Change + * - 05/16/14 + - Updated :ref:`Working with Video Components` to reflect UI changes. * - 05/14/14 - Updated the :ref:`Running Your Course Index` chapter to remove references to the "new beta" Instructor Dashboard. - * - + * - 05/13/14 - Updated the :ref:`Enrollment` section to reflect that usernames or email addresses can be used to batch enroll students. * - diff --git a/docs/en_us/course_authors/source/creating_content/create_video.rst b/docs/en_us/course_authors/source/creating_content/create_video.rst index d818edeb4a..57fcb9b040 100644 --- a/docs/en_us/course_authors/source/creating_content/create_video.rst +++ b/docs/en_us/course_authors/source/creating_content/create_video.rst @@ -113,16 +113,15 @@ company that provides captioning services. EdX works with `3Play Media `_. `YouTube `_ also provides captioning services. -In addition to your .srt file, you can provide other transcripts with your -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`. +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`. -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) 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 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 you also post copies of your videos on a third-party site such as `Amazon 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 -doesn’t play, the video on the backup site starts playing automatically. The -student can also click a link to download the video from the backup site. +doesn’t play, the video on the backup site starts playing automatically. You can also allow the +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 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. .mp4, .mpeg, .ogg, or .webm. EdX can't support videos that you post on sites such as Vimeo. - - .. _Create a Video Component: ******************************** @@ -186,42 +183,44 @@ Step 4. Create a Video Component .. image:: ../Images/VideoComponentEditor.png :alt: Image of the video component editor + :width: 500 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 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. :: http://youtu.be/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 using a video from an existing course--Studio automatically finds the transcript and associates the transcript with the video. - If you want to modify the transcript, click **Download to Edit**. You can - then make your changes and upload the new file by clicking **Upload New - Timed Transcript**. + 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**. - - 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, click **Import from YouTube**. (If you want to modify the - YouTube transcript, after Studio imports the transcript, click **Download - to Edit**. You can then make your changes and upload the new file by - clicking **Upload New Timed Transcript**.) + transcript, click **Import YouTube Transcript**. (If you want to modify the + 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 + clicking **Upload New 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 - 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. .. note:: @@ -229,13 +228,13 @@ Step 4. Create a Video Component * If your transcript uses the .sjson format, do not use this setting. 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 information, see :ref:`Additional Transcripts`. #. 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.** @@ -247,61 +246,39 @@ Advanced Options 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 - same as the **Display Name** field on the **Basic** tab. +.. list-table:: + :widths: 30 70 -* **Download Transcript**: The URL for the transcript file for the video. This - file is usually an .srt file, but can also be a .txt or .pdf file. (For more - information about .txt and .pdf files, see :ref:`Additional Transcripts`.) The - URL can be an external URL, such as **http://example.org/transcript.srt**, or - the URL for a file that you've uploaded to your **Files & Uploads** page, such - as **/static/example.srt**. + * - **Component Display Name** + - The name that you want your students to see. This is the same as the **Display Name** field on the **Basic** tab. + * - **Default Timed Transcript** + - 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. + * - **Download Transcript Allowed** + - 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 - file in the **Download Transcript** field, the file you've specified will be - available for students to download. + * - **Downloadable Transcript URL** + - 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. - * If you set **Transcript Download Allowed** to **True**, but you leave the - **Download Transcript** field blank, the .srt transcript that automatically - plays with the video will be available. - -* **End Time**: The time, formatted as hours, minutes, and seconds (HH:MM:SS), - when you want the video to end. - -* **Start Time**: The time, formatted as hours, minutes, and seconds (HH:MM:SS), - when you want the video to begin. - -* **Transcript (primary)**: The name of the .srt file from the **Timed - Transcript** field on the **Basic** tab. This field is auto-populated. You - don't have to change this setting. - - If your transcript uses an .sjson file, see :ref:`Steps for sjson files`. - -* **Transcript Display**: Specifies whether you want the transcript to show by - default. Students can always turn transcripts on or off while they watch the - video. - - -* **Transcript Download Allowed**: Specifies whether you want to allow your - students to download a copy of the transcript. - -* **Transcript Translations**: The transcript files for any additional - languages. For more information, see :ref:`Transcripts in Additional - Languages`. - -* **Video Download Allowed**: Specifies whether you want to allow your students - to download a copy of the video. - -* **Video Sources**: Additional locations where you've posted the video. This - 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 - ID for 1.5x speed**: If you have uploaded separate videos to YouTube for - different speeds of your video, enter the YouTube IDs for these videos in - these fields. + .. 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`. + * - **Show Transcript** + - Specifies whether the transcript plays along with the video by default. + * - **Transcript Languages** + - The transcript files for any additional languages. For more information, see :ref:`Transcripts in Additional Languages`. + * - **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. + * - **Video Download Allowed** + - 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** + - 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**. + * - **Video Start Time** + - 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** + - 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** + - 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. .. _Additional Transcripts: @@ -309,16 +286,35 @@ The following options appear on the **Advanced** tab in the Video component. Additional Transcripts ********************** -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. +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. -#. Upload the .txt or .pdf transcript to the **Files & Uploads** page or host it - on an external website. +.. image:: /Images/Video_DownTrans_srt-txt.png + :width: 500 + :alt: Video status bar showing srt and txt transcript download options +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. + +.. image:: /Images/Video_DownTrans_srt-handout.png + :width: 500 + :alt: Video status bar showing srt, txt, and handout transcript download options + +To add a downloadable transcript by using the **Upload Handout** field: + +#. 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**. +#. 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**. -#. In the **Download Transcript** field, enter the URL for the transcript. For - more information, see :ref:`Video Advanced Options`. + +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. + +.. image:: /Images/Video_DownTrans_other.png + :width: 500 + :alt: Video status bar showing Download Transcript button without srt and txt options + +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. .. _Transcripts in Additional Languages: @@ -381,7 +377,7 @@ the Video component. #. Upload the .sjson file for your video to the **Files & Uploads** page. #. Create a new video component. #. 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 may resemble one of the following. @@ -392,7 +388,7 @@ the Video component. https://s3.amazonaws.com/edx-course-videos/edx-edx101/EDXSPCPJSP13-G030300.mp4 #. 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 enter **Lecture1a**. #. Set the other options that you want. From 8afe315ce43fcb0487cccebf255ce3dc81782a66 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 16 May 2014 13:59:21 -0400 Subject: [PATCH 17/34] link to code considerations page from contributor doc --- docs/en_us/developers/source/process/contributor.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en_us/developers/source/process/contributor.rst b/docs/en_us/developers/source/process/contributor.rst index 271dd8215d..fe7730337b 100644 --- a/docs/en_us/developers/source/process/contributor.rst +++ b/docs/en_us/developers/source/process/contributor.rst @@ -100,6 +100,7 @@ Further Information For futher information on the pull request requirements, please see the following links: +* :doc:`../code-considerations` * :doc:`../testing` * :doc:`../testing/jenkins` * :doc:`../testing/code-coverage` From d4280e8529942cd8cc00c5528836b8bbc151e1a3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 16 May 2014 14:10:50 -0400 Subject: [PATCH 18/34] Fix merge conflicts --- cms/djangoapps/contentstore/tests/utils.py | 3 --- .../contentstore/views/import_export.py | 20 +++---------------- .../views/tests/test_import_export.py | 3 --- common/djangoapps/student/views.py | 4 ---- .../class_dashboard/dashboard_data.py | 5 ----- .../instructor/views/instructor_dashboard.py | 7 +------ lms/static/js/staff_debug_actions.js | 19 +++--------------- lms/templates/notes.html | 11 +--------- lms/templates/staff_problem_info.html | 16 +++------------ 9 files changed, 11 insertions(+), 77 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 1224921171..d648942023 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -13,10 +13,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.utils import get_modulestore -<<<<<<< HEAD -======= from student.models import Registration ->>>>>>> edx/master def parse_json(response): diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index ebd19040bb..27b6b6de00 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -22,20 +22,13 @@ 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 xmodule.contentstore.django import contentstore -<<<<<<< HEAD -from xmodule.modulestore.xml_exporter import export_to_xml +from xmodule.exceptions import SerializationError from xmodule.modulestore.django import modulestore from xmodule.modulestore.keys import CourseKey -from xmodule.exceptions import SerializationError - -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 .access import has_course_access from .access import has_course_access from extract_tar import safetar_extractall @@ -240,13 +233,6 @@ def import_handler(request, course_key_string): session_status[key] = 3 request.session.modified = True -<<<<<<< HEAD - auth.add_users(request.user, CourseInstructorRole(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)) - -======= ->>>>>>> edx/master # Send errors to client with stage at which error occurred. except Exception as exception: # pylint: disable=W0703 log.exception( diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 0a7efaf66b..a2d41d7cdd 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -14,10 +14,7 @@ from uuid import uuid4 from django.test.utils import override_settings from django.conf import settings -<<<<<<< HEAD from contentstore.utils import reverse_course_url -======= ->>>>>>> edx/master from xmodule.contentstore.django import _CONTENTSTORE from xmodule.modulestore.django import loc_mapper diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index a14243c767..3e35054ad1 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1868,11 +1868,7 @@ def token(request): the token was issued. This will be stored with the user along with the id for identification purposes in the backend. ''' -<<<<<<< HEAD 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) dtnow = datetime.datetime.now() dtutcnow = datetime.datetime.utcnow() diff --git a/lms/djangoapps/class_dashboard/dashboard_data.py b/lms/djangoapps/class_dashboard/dashboard_data.py index 333a99eba9..fe4a1a74fa 100644 --- a/lms/djangoapps/class_dashboard/dashboard_data.py +++ b/lms/djangoapps/class_dashboard/dashboard_data.py @@ -433,12 +433,7 @@ def get_students_opened_subsection(request, csv=False): If 'csv' is True, returns a header array, and an array of arrays in the format: student names, usernames for CSV download. """ -<<<<<<< HEAD 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') # Query for "opened a subsection" students diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 25ade4f771..4b3e95f05f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -255,14 +255,9 @@ def _section_metrics(course_key, access): 'section_key': 'metrics', 'section_display_name': ('Metrics'), 'access': access, -<<<<<<< HEAD + 'course_id': course_key.to_deprecated_string(), 'sub_section_display_name': get_section_display_name(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_problem_grades_url': reverse('get_students_problem_grades'), 'post_metrics_data_csv_url': reverse('post_metrics_data_csv'), diff --git a/lms/static/js/staff_debug_actions.js b/lms/static/js/staff_debug_actions.js index 7464a40346..c5a49f39fb 100644 --- a/lms/static/js/staff_debug_actions.js +++ b/lms/static/js/staff_debug_actions.js @@ -114,29 +114,16 @@ var StaffDebug = (function(){ // Register click handlers $(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')); return false; }); - $('#staff-debug-sdelete').click(function() { + $('.staff-debug-sdelete').click(function() { StaffDebug.sdelete($(this).parent().data('location-name'), $(this).parent().data('location')); return false; }); - $('#staff-debug-rescore').click(function() { - 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 + StaffDebug.rescore($(this).parent().data('location-name'), $(this).parent().data('location')); return false; }); }); diff --git a/lms/templates/notes.html b/lms/templates/notes.html index 35c0e8d305..abb168c975 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -66,19 +66,10 @@

- <%include file="widgets/sock.html" /> + <%include file="widgets/sock.html" args="online_help_token=online_help_token" /> % endif <%include file="widgets/footer.html" /> diff --git a/cms/templates/index.html b/cms/templates/index.html index fba662735d..2a905a75ae 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -275,9 +275,6 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) { % endif - <% - help_doc_urls = get_online_help_info(online_help_token()) - %>
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 1d97df3916..24dbe42496 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -124,13 +124,9 @@ % if user.is_authenticated():