Commit 30f2c933 by Calen Pennington

Merge pull request #3763 from cpennington/opaque-keys-merge-master

Merge latest master into opaque-keys
parents 1c6a0f0a a3b6ef8b
import ConfigParser
from django.conf import settings
import logging
log = logging.getLogger(__name__)
# Open and parse the configuration file when the module is initialized
config_file = open(settings.REPO_ROOT / "docs" / "config.ini")
config = ConfigParser.ConfigParser()
config.readfp(config_file)
def doc_url(request):
# in the future, we will detect the locale; for now, we will
# hardcode en_us, since we only have English documentation
locale = "en_us"
def doc_url(request=None): # pylint: disable=unused-argument
"""
This function is added in the list of TEMPLATE_CONTEXT_PROCESSORS, which is a django setting for
a tuple of callables that take a request object as their argument and return a dictionary of items
to be merged into the RequestContext.
This function returns a dict with get_online_help_info, making it directly available to all mako templates.
Args:
request: Currently not used, but is passed by django to context processors.
May be used in the future for determining the language of choice.
"""
def get_online_help_info(page_token=None):
"""
Args:
page_token: A string that identifies the page for which the help information is requested.
It should correspond to an option in the docs/config.ini file. If it doesn't, the "default"
option is used instead.
Returns:
A dict mapping the following items
* "doc_url" - a string with the url corresponding to the online help location for the given page_token.
* "pdf_url" - a string with the url corresponding to the location of the PDF help file.
"""
def get_config_value_with_default(section_name, option, default_option="default"):
"""
Args:
section_name: name of the section in the configuration from which the option should be found
option: name of the configuration option
default_option: name of the default configuration option whose value should be returned if the
requested option is not found
"""
try:
return config.get(section_name, option)
except (ConfigParser.NoOptionError, AttributeError):
log.debug("Didn't find a configuration option for '%s' section and '%s' option", section_name, option)
return config.get(section_name, default_option)
def get_doc_url():
"""
Returns:
The URL for the documentation
"""
return "{url_base}/{language}/{version}/{page_path}".format(
url_base=config.get("help_settings", "url_base"),
language=get_config_value_with_default("locales", settings.LANGUAGE_CODE),
version=config.get("help_settings", "version"),
page_path=get_config_value_with_default("pages", page_token),
)
def get_pdf_url():
"""
Returns:
The URL for the PDF document using the pdf_settings and the help_settings (version) in the configuration
"""
return "{pdf_base}/{version}/{pdf_file}".format(
pdf_base=config.get("pdf_settings", "pdf_base"),
version=config.get("help_settings", "version"),
pdf_file=config.get("pdf_settings", "pdf_file"),
)
def get_doc_url(token):
try:
return config.get(locale, token)
except ConfigParser.NoOptionError:
return config.get(locale, "default")
return {
"doc_url": get_doc_url(),
"pdf_url": get_pdf_url(),
}
return {"doc_url": get_doc_url}
return {'get_online_help_info': get_online_help_info}
# disable missing docstring
# pylint: disable=C0111
# pylint: disable=W0621
# pylint: disable=W0613
from lettuce import world, step
from component_settings_editor_helpers import enter_xml_in_advanced_problem
......@@ -8,11 +9,16 @@ from xmodule.modulestore.locations import SlashSeparatedCourseKey
from contentstore.utils import reverse_usage_url
@step('I export the course$')
def i_export_the_course(step):
@step('I go to the export page$')
def i_go_to_the_export_page(step):
world.click_tools()
link_css = 'li.nav-course-tools-export a'
world.css_click(link_css)
@step('I export the course$')
def i_export_the_course(step):
step.given('I go to the export page')
world.css_click('a.action-export')
......@@ -32,7 +38,7 @@ def i_enter_bad_xml(step):
@step('I edit and enter an ampersand$')
def i_enter_bad_xml(step):
def i_enter_an_ampersand(step):
enter_xml_in_advanced_problem(step, "<problem>&</problem>")
......
# pylint: disable=C0111
# pylint: disable=W0621
# pylint: disable=W0613
import os
from lettuce import world
from lettuce import world, step
from django.conf import settings
......@@ -14,7 +18,8 @@ def import_file(filename):
world.css_click(outline_css)
def go_to_import():
@step('I go to the import page$')
def go_to_import(step):
menu_css = 'li.nav-course-tools'
import_css = 'li.nav-course-tools-import a'
world.css_click(menu_css)
......
@shard_1
Feature: CMS.Help
As a course author, I am able to access online help
Scenario: Users can access online help on course listing page
Given There are no courses
And I am logged into Studio
Then I should see online help for "get_started"
Scenario: Users can access online help within a course
Given I have opened a new course in Studio
And I click the course link in My Courses
Then I should see online help for "organizing_course"
And I go to the course updates page
Then I should see online help for "updates"
And I go to the pages page
Then I should see online help for "pages"
And I go to the files and uploads page
Then I should see online help for "files"
And I go to the textbooks page
Then I should see online help for "textbooks"
And I select Schedule and Details
Then I should see online help for "setting_up"
And I am viewing the grading settings
Then I should see online help for "grading"
And I am viewing the course team settings
Then I should see online help for "course-team"
And I select the Advanced Settings
Then I should see online help for "index"
And I select Checklists from the Tools menu
Then I should see online help for "checklist"
And I go to the import page
Then I should see online help for "import"
And I go to the export page
Then I should see online help for "export"
Scenario: Users can access online help on the unit page
Given I am in Studio editing a new unit
Then I should see online help for "units"
Scenario: Users can access online help on the subsection page
Given I have opened a new course section in Studio
And I have added a new subsection
And I click on the subsection
Then I should see online help for "subsections"
# pylint: disable=C0111
# pylint: disable=W0621
# pylint: disable=W0613
from nose.tools import assert_false # pylint: disable=no-name-in-module
from lettuce import step, world
@step(u'I should see online help for "([^"]*)"$')
def see_online_help_for(step, page_name):
# make sure the online Help link exists on this page and contains the expected page name
elements_found = world.browser.find_by_xpath(
'//li[contains(@class, "nav-account-help")]//a[contains(@href, "{page_name}")]'.format(
page_name=page_name
)
)
assert_false(elements_found.is_empty())
# make sure the PDF link on the sock of this page exists
# for now, the PDF link stays constant for all the pages so we just check for "pdf"
elements_found = world.browser.find_by_xpath(
'//section[contains(@class, "sock")]//li[contains(@class, "js-help-pdf")]//a[contains(@href, "pdf")]'
)
assert_false(elements_found.is_empty())
......@@ -6,7 +6,7 @@ from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from common import type_in_codemirror, open_new_course
from advanced_settings import change_value
from course_import import import_file, go_to_import
from course_import import import_file
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
......@@ -218,11 +218,6 @@ def i_have_empty_course(step):
open_new_course()
@step(u'I go to the import page')
def i_go_to_import(_step):
go_to_import()
@step(u'I import the file "([^"]*)"$')
def i_import_the_file(_step, filename):
import_file(filename)
......
......@@ -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
......@@ -4,15 +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 student.models import Registration
def parse_json(response):
......@@ -93,9 +94,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'
......@@ -108,7 +109,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):
......
......@@ -4,38 +4,37 @@ 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.exceptions import SerializationError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.keys import CourseKey
from xmodule.exceptions import SerializationError
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_exporter import export_to_xml
from .access import has_course_access
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
from contentstore.utils import reverse_course_url, reverse_usage_url
......@@ -234,10 +233,6 @@ def import_handler(request, course_key_string):
session_status[key] = 3
request.session.modified = True
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))
# Send errors to client with stage at which error occurred.
except Exception as exception: # pylint: disable=W0703
log.exception(
......
"""
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 contentstore.utils import reverse_course_url
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
......@@ -105,6 +109,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.id), nonstaff_user)
course = self.store.get_course(self.course.id)
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_course(self.course.id)
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.id).has_user(nonstaff_user))
self.assertTrue(CourseStaffRole(self.course.id).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.id).has_user(nonstaff_user))
self.assertTrue(CourseStaffRole(self.course.id).has_user(nonstaff_user))
## Unsafe tar methods #####################################################
# Each of these methods creates a tarfile with a single type of unsafe
# content.
......
......@@ -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',
},
......
......@@ -4,6 +4,10 @@ Specific overrides to the base prod settings to make development easier.
from .aws import * # pylint: disable=wildcard-import, unused-wildcard-import
# Don't use S3 in devstack, fall back to filesystem
del DEFAULT_FILE_STORAGE
MEDIA_ROOT = "/edx/var/edxapp/uploads"
DEBUG = True
USE_I18N = True
TEMPLATE_DEBUG = DEBUG
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "files" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
......
......@@ -264,7 +264,8 @@
<!-- view -->
<div class="wrapper wrapper-view">
<%include file="widgets/header.html" />
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
<%include file="widgets/header.html" args="online_help_token=online_help_token" />
<div id="page-alert"></div>
......@@ -276,7 +277,7 @@
<script type="text/javascript">
require(['js/sock']);
</script>
<%include file="widgets/sock.html" />
<%include file="widgets/sock.html" args="online_help_token=online_help_token" />
% endif
<%include file="widgets/footer.html" />
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "checklist" %></%def>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
......
......@@ -2,6 +2,7 @@
from django.utils.translation import ugettext as _
%>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "updates" %></%def>
<%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title -->
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "pages" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import ugettext as _
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "subsection" %></%def>
<%!
import logging
from util.date_utils import get_default_time_display, almost_same_datetime
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "export" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "import" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import ugettext as _
......
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "home" %></%def>
<%block name="title">${_("My Courses")}</%block>
<%block name="bodyclass">is-signedin index view-dashboard</%block>
......@@ -275,18 +275,18 @@ require(["domReady!", "jquery", "jquery.form", "js/index"], function(doc, $) {
% endif
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title title-3">${_('Need help?')}</h3>
<p>${_('If you are new to Studio and having trouble getting started, there are a few things that may be of help:')}</p>
<h3 class="title title-3">${_('New to edX Studio?')}</h3>
<p>${_('Click Help in the upper-right corner to get more more information about the Studio page you are viewing. You can also use the links at the bottom of the page to access our continously updated documentation and other Studio resources.')}</p>
<ol class="list-actions">
<li class="action-item">
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" title="This is a PDF Document">${_('Get started by reading Studio\'s Documentation')}</a>
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Getting Started with edX Studio")}</a>
</li>
<li class="action-item">
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to request help">${_('Request help with Studio')}</a>
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="Use our feedback tool, Tender, to request help">${_('Request help with edX Studio')}</a>
</li>
</ol>
</div>
......
......@@ -3,6 +3,7 @@
<%! from django.core.urlresolvers import reverse %>
<%! from student.roles import CourseInstructorRole %>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "team" %></%def>
<%block name="title">${_("Course Team Settings")}</%block>
<%block name="bodyclass">is-signedin course users view-team</%block>
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "outline" %></%def>
<%!
import logging
from util.date_utils import get_default_time_display
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "schedule" %></%def>
<%block name="title">${_("Schedule &amp; Details Settings")}</%block>
<%block name="bodyclass">is-signedin course schedule view-settings feature-upload</%block>
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "advanced" %></%def>
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import ugettext as _
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "grading" %></%def>
<%block name="title">${_("Grading Settings")}</%block>
<%block name="bodyclass">is-signedin course grading view-settings</%block>
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "textbooks" %></%def>
<%namespace name='static' file='static_content.html'/>
<%! import json %>
<%! from django.utils.translation import ugettext as _ %>
......@@ -74,6 +75,7 @@ require(["js/models/section", "js/collections/textbook", "js/views/list_textbook
<div class="bit">
<h3 class="title-3">${_("What if my book isn't divided into chapters?")}</h3>
<p>${_("If your textbook doesn't have individual chapters, you can upload the entire text as a single chapter and enter a name of your choice in the Chapter Name field.")}</p>
<p><a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Learn More")}</a></p>
</div>
</aside>
</section>
......
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "unit" %></%def>
<%!
from contentstore import utils
from contentstore.views.helpers import EDITING_TEMPLATES
......
......@@ -2,7 +2,9 @@
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from contentstore.context_processors import doc_url
%>
<%page args="online_help_token"/>
<div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner">
......@@ -120,26 +122,9 @@
% if user.is_authenticated():
<nav class="nav-account nav-is-signedin nav-dd ui-right">
<h2 class="sr">${_("Help &amp; Account Navigation")}</h2>
<ol>
<li class="nav-item nav-account-help">
<h3 class="title"><span class="label">${_("Help")}</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
<li class="nav-item nav-help-documentation">
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" title="${_("This is a PDF Document")}">${_("Studio Documentation")}</a>
</li>
<li class="nav-item nav-help-helpcenter">
<a href="http://help.edge.edx.org/" rel="external">${_("Studio Help Center")}</a>
</li>
<li class="nav-item nav-help-feedback">
<a href="http://help.edge.edx.org/discussion/new" class="show-tender" title="${_("Use our feedback tool, Tender, to share your feedback")}">${_("Contact Us")}</a>
</li>
</ul>
</div>
</div>
<h3 class="title"><span class="label"><a href="${get_online_help_info(online_help_token)['doc_url']}" title="${_("Contextual Online Help")}" target="${_("_blank")}">${_("Help")}</a></span></h3>
</li>
<li class="nav-item nav-account-user">
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="online_help_token"/>
<div class="wrapper-sock wrapper">
<ul class="list-actions list-cta">
<li class="action-item">
<a href="#sock" class="cta cta-show-sock"><i class="icon-question-sign"></i> <span class="copy">${_("Looking for Help with Studio?")}</span></a>
<a href="#sock" class="cta cta-show-sock"><i class="icon-question-sign"></i> <span class="copy">${_("Looking for help with Studio?")}</span></a>
</li>
</ul>
<div class="wrapper-inner wrapper">
<section class="sock" id="sock">
<header>
<h2 class="title sr">${_("edX Studio Help")}</h2>
<h2 class="title sr">${_("edX Studio Documentation")}</h2>
</header>
<div class="support">
<h3 class="title">${_("Studio Support")}</h3>
<h3 class="title">${_("edX Studio Documentation")}</h3>
<div class="copy">
<p>${_("Need help with Studio? Creating a course is complex, so we're here to help. Take advantage of our documentation, help center, as well as our edX101 introduction course for course authors.")}</p>
<p>${_("You can click Help in the upper right corner of any page to get more information about the page you're on. You can also use the links below to download the Building and Running an edX Course PDF file, to go to the edX Author Support site, or to enroll in edX101.")}</p>
</div>
<ul class="list-actions">
<li class="action-item js-help-pdf">
<a href="${get_online_help_info(online_help_token)['pdf_url']}" target="_blank" rel="external" class="action action-primary">${_("Building and Running an edX Course PDF")}</a>
</li>
<li class="action-item">
<a href="http://files.edx.org/Getting_Started_with_Studio.pdf" class="action action-primary" title="${_("This is a PDF Document")}">${_("Download Studio Documentation")}</a>
<span class="tip">${_("How to use Studio to build your course")}</span>
</li>
<li class="action-item">
<a href="http://help.edge.edx.org/" rel="external" class="action action-primary">${_("Studio Help Center")}</a>
<span class="tip">${_("Studio Help Center")}</span>
<a href="http://help.edge.edx.org/" rel="external" class="action action-primary">${_("edX Studio Author Support")}</a>
<span class="tip">${_("edX Studio Author Support")}</span>
</li>
<li class="action-item">
<a href="https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about" rel="external" class="action action-primary">${_("Enroll in edX101")}</a>
<span class="tip">${_("How to use Studio to build your course")}</span>
<span class="tip">${_("How to use edX Studio to build your course")}</span>
</li>
</ul>
</div>
<div class="feedback">
<h3 class="title">${_("Contact us about Studio")}</h3>
<h3 class="title">${_("Request help with edX Studio")}</h3>
<div class="copy">
<p>${_("Have problems, questions, or suggestions about Studio? We're also here to listen to any feedback you want to share.")}</p>
<p>${_("Have problems, questions, or suggestions about edX Studio?")}</p>
</div>
<ul class="list-actions">
......
'''
Firebase - library to generate a token
License: https://github.com/firebase/firebase-token-generator-python/blob/master/LICENSE
Tweaked and Edited by @danielcebrianr and @lduarte1991
This library will take either objects or strings and use python's built-in encoding
system as specified by RFC 3548. Thanks to the firebase team for their open-source
library. This was made specifically for speaking with the annotation_storage_url and
can be used and expanded, but not modified by anyone else needing such a process.
'''
from base64 import urlsafe_b64encode
import hashlib
import hmac
import sys
try:
import json
except ImportError:
import simplejson as json
__all__ = ['create_token']
TOKEN_SEP = '.'
def create_token(secret, data):
'''
Simply takes in the secret key and the data and
passes it to the local function _encode_token
'''
return _encode_token(secret, data)
if sys.version_info < (2, 7):
def _encode(bytes_data):
'''
Takes a json object, string, or binary and
uses python's urlsafe_b64encode to encode data
and make it safe pass along in a url.
To make sure it does not conflict with variables
we make sure equal signs are removed.
More info: docs.python.org/2/library/base64.html
'''
encoded = urlsafe_b64encode(bytes(bytes_data))
return encoded.decode('utf-8').replace('=', '')
else:
def _encode(bytes_info):
'''
Same as above function but for Python 2.7 or later
'''
encoded = urlsafe_b64encode(bytes_info)
return encoded.decode('utf-8').replace('=', '')
def _encode_json(obj):
'''
Before a python dict object can be properly encoded,
it must be transformed into a jason object and then
transformed into bytes to be encoded using the function
defined above.
'''
return _encode(bytearray(json.dumps(obj), 'utf-8'))
def _sign(secret, to_sign):
'''
This function creates a sign that goes at the end of the
message that is specific to the secret and not the actual
content of the encoded body.
More info on hashing: http://docs.python.org/2/library/hmac.html
The function creates a hashed values of the secret and to_sign
and returns the digested values based the secure hash
algorithm, 256
'''
def portable_bytes(string):
'''
Simply transforms a string into a bytes object,
which is a series of immutable integers 0<=x<=256.
Always try to encode as utf-8, unless it is not
compliant.
'''
try:
return bytes(string, 'utf-8')
except TypeError:
return bytes(string)
return _encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), hashlib.sha256).digest()) # pylint: disable=E1101
def _encode_token(secret, claims):
'''
This is the main function that takes the secret token and
the data to be transmitted. There is a header created for decoding
purposes. Token_SEP means that a period/full stop separates the
header, data object/message, and signatures.
'''
encoded_header = _encode_json({'typ': 'JWT', 'alg': 'HS256'})
encoded_claims = _encode_json(claims)
secure_bits = '%s%s%s' % (encoded_header, TOKEN_SEP, encoded_claims)
sig = _sign(secret, secure_bits)
return '%s%s%s' % (secure_bits, TOKEN_SEP, sig)
"""
This test will run for firebase_token_generator.py.
"""
from django.test import TestCase
from student.firebase_token_generator import _encode, _encode_json, _encode_token, create_token
class TokenGenerator(TestCase):
"""
Tests for the file firebase_token_generator.py
"""
def test_encode(self):
"""
This tests makes sure that no matter what version of python
you have, the _encode function still returns the appropriate result
for a string.
"""
expected = "dGVzdDE"
result = _encode("test1")
self.assertEqual(expected, result)
def test_encode_json(self):
"""
Same as above, but this one focuses on a python dict type
transformed into a json object and then encoded.
"""
expected = "eyJ0d28iOiAidGVzdDIiLCAib25lIjogInRlc3QxIn0"
result = _encode_json({'one': 'test1', 'two': 'test2'})
self.assertEqual(expected, result)
def test_create_token(self):
"""
Unlike its counterpart in student/views.py, this function
just checks for the encoding of a token. The other function
will test depending on time and user.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VySWQiOiAidXNlcm5hbWUiLCAidHRsIjogODY0MDB9.-p1sr7uwCapidTQ0qB7DdU2dbF-hViKpPNN_5vD10t8"
result1 = _encode_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
result2 = create_token('4c7f4d1c-8ac4-4e9f-84c8-b271c57fcac4', {"userId": "username", "ttl": 86400})
self.assertEqual(expected, result1)
self.assertEqual(expected, result2)
......@@ -27,7 +27,7 @@ from mock import Mock, patch
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
......@@ -491,3 +491,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])
......@@ -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
......
......@@ -1796,7 +1796,7 @@ class SymbolicResponse(CustomResponse):
log.error(traceback.format_exc())
_ = self.capa_system.i18n.ugettext
# Translators: 'SymbolicResponse' is a problem type and should not be translated.
msg = _(u"oops in SymbolicResponse (cfn) error {error_msg}").format(
msg = _(u"An error occurred with SymbolicResponse. The error was: {error_msg}").format(
error_msg=err,
)
raise Exception(msg)
......
"""
This file contains a function used to retrieve the token for the annotation backend
without having to create a view, but just returning a string instead.
It can be called from other files by using the following:
from xmodule.annotator_token import retrieve_token
"""
import datetime
from firebase_token_generator import create_token
def retrieve_token(userid, secret):
'''
Return a token for the backend of annotations.
It uses the course id to retrieve a variable that contains the secret
token found in inheritance.py. It also contains information of when
the token was issued. This will be stored with the user along with
the id for identification purposes in the backend.
'''
# the following five lines of code allows you to include the default timezone in the iso format
# for more information: http://stackoverflow.com/questions/3401428/how-to-get-an-isoformat-datetime-string-including-the-default-timezone
dtnow = datetime.datetime.now()
dtutcnow = datetime.datetime.utcnow()
delta = dtnow - dtutcnow
newhour, newmin = divmod((delta.days * 24 * 60 * 60 + delta.seconds + 30) // 60, 60)
newtime = "%s%+02d:%02d" % (dtnow.isoformat(), newhour, newmin)
# uses the issued time (UTC plus timezone), the consumer key and the user's email to maintain a
# federated system in the annotation backend server
custom_data = {"issuedAt": newtime, "consumerKey": secret, "userId": userid, "ttl": 86400}
newtoken = create_token(secret, custom_data)
return newtoken
"""
This test will run for annotator_token.py
"""
import unittest
from xmodule.annotator_token import retrieve_token
class TokenRetriever(unittest.TestCase):
"""
Tests to make sure that when passed in a username and secret token, that it will be encoded correctly
"""
def test_token(self):
"""
Test for the token generator. Give an a random username and secret token, it should create the properly encoded string of text.
"""
expected = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpc3N1ZWRBdCI6ICIyMDE0LTAyLTI3VDE3OjAwOjQyLjQwNjQ0MSswOjAwIiwgImNvbnN1bWVyS2V5IjogImZha2Vfc2VjcmV0IiwgInVzZXJJZCI6ICJ1c2VybmFtZSIsICJ0dGwiOiA4NjQwMH0.Dx1PoF-7mqBOOSGDMZ9R_s3oaaLRPnn6CJgGGF2A5CQ"
response = retrieve_token("username", "fake_secret")
self.assertEqual(expected.split('.')[0], response.split('.')[0])
self.assertNotEqual(expected.split('.')[2], response.split('.')[2])
\ No newline at end of file
......@@ -38,6 +38,17 @@ class TextAnnotationModuleTestCase(unittest.TestCase):
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)
......@@ -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 = '<annotation title="x" body="y" problem="0">test</annotation>'
element = etree.fromstring(xml)
expected_attr = {'class': {'value': 'annotatable-span highlight'}}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_valid_highlight(self):
"""
Same as above but more specific to an area that is highlightable in the appropriate
color designated.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for color in self.mod.highlight_colors:
element = etree.fromstring(xml.format(highlight=color))
value = 'annotatable-span highlight highlight-{highlight}'.format(highlight=color)
expected_attr = {'class': {
'value': value,
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_class_attr_with_invalid_highlight(self):
"""
Same as above, but checked with invalid colors.
"""
xml = '<annotation title="x" body="y" problem="0" highlight="{highlight}">test</annotation>'
for invalid_color in ['rainbow', 'blink', 'invisible', '', None]:
element = etree.fromstring(xml.format(highlight=invalid_color))
expected_attr = {'class': {
'value': 'annotatable-span highlight',
'_delete': 'highlight'}
}
actual_attr = self.mod._get_annotation_class_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_annotation_data_attr(self):
"""
Test that each highlight contains the data information from the annotation itself.
"""
element = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
expected_attr = {
'data-comment-body': {'value': 'foo', '_delete': 'body'},
'data-comment-title': {'value': 'bar', '_delete': 'title'},
'data-problem-id': {'value': '0', '_delete': 'problem'}
}
actual_attr = self.mod._get_annotation_data_attr(element) # pylint: disable=W0212
self.assertIsInstance(actual_attr, dict)
self.assertDictEqual(expected_attr, actual_attr)
def test_render_annotation(self):
"""
Tests to make sure that the spans designating annotations acutally visually render as annotations.
"""
expected_html = '<span class="annotatable-span highlight highlight-yellow" data-comment-title="x" data-comment-body="y" data-problem-id="0">z</span>'
expected_el = etree.fromstring(expected_html)
actual_el = etree.fromstring('<annotation title="x" body="y" problem="0" highlight="yellow">z</annotation>')
self.mod._render_annotation(actual_el) # pylint: disable=W0212
self.assertEqual(expected_el.tag, actual_el.tag)
self.assertEqual(expected_el.text, actual_el.text)
self.assertDictEqual(dict(expected_el.attrib), dict(actual_el.attrib))
def test_render_content(self):
"""
Like above, but using the entire text, it makes sure that display_name is removed and that there is only one
div encompassing the annotatable area.
"""
content = self.mod._render_content() # pylint: disable=W0212
element = etree.fromstring(content)
self.assertIsNotNone(element)
self.assertEqual('div', element.tag, 'root tag is a div')
self.assertFalse('display_name' in element.attrib, "Display Name should have been deleted from Content")
def test_extract_instructions(self):
"""
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)
......@@ -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 <instructions> from the xmltree and returns them as a string, otherwise None. """
......@@ -77,14 +81,15 @@ class TextAnnotationModule(AnnotatableFields, XModule):
def get_html(self):
""" Renders parameters to template. """
context = {
'course_key': self.runtime.course_id,
'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 +102,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
......@@ -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 <instructions> from the xmltree and returns them as a string, otherwise None. """
......@@ -86,14 +149,15 @@ class VideoAnnotationModule(AnnotatableFields, XModule):
extension = self._get_extension(self.sourceurl)
context = {
'course_key': self.runtime.course_id,
'display_name': self.display_name_with_default,
'instructions_html': self.instructions,
'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 +172,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
Annotator.Plugin.Auth.prototype.haveValidToken = function() {
return (
this._unsafeToken &&
this._unsafeToken.d.issuedAt &&
this._unsafeToken.d.ttl &&
this._unsafeToken.d.consumerKey &&
this.timeToExpiry() > 0
);
};
Annotator.Plugin.Auth.prototype.timeToExpiry = function() {
var expiry, issue, now, timeToExpiry;
now = new Date().getTime() / 1000;
issue = createDateFromISO8601(this._unsafeToken.d.issuedAt).getTime() / 1000;
expiry = issue + this._unsafeToken.d.ttl;
timeToExpiry = expiry - now;
if (timeToExpiry > 0) {
return timeToExpiry;
} else {
return 0;
}
};
\ No newline at end of file
......@@ -64,7 +64,7 @@ class RegistrationTest(UniqueCourseTest):
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
......@@ -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):
"""
......
......@@ -10,6 +10,7 @@
# abdallah.nassif <abdallah_n@hotmail.com>, 2013
# Ahmad Abd Arrahman <mygooglizer@gmail.com>, 2013
# Ahmad Abd Arrahman <mygooglizer@gmail.com>, 2013
# ayshibly <ayshibly@gmail.com>, 2014
# jkfreij <jkfreij@gmail.com>, 2014
# khateeb <eng.elkhteeb@gmail.com>, 2013
# khateeb <eng.elkhteeb@gmail.com>, 2013
......@@ -37,8 +38,8 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"PO-Revision-Date: 2014-05-12 17:46+0000\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-18 11:56+0000\n"
"Last-Translator: nabeelqordoba <nabeel@qordoba.com>\n"
"Language-Team: Arabic (http://www.transifex.com/projects/p/edx-platform/language/ar/)\n"
"MIME-Version: 1.0\n"
......@@ -1552,6 +1553,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr "لا يمكن استرجاع البيانات، الرجاء إعادة المحاولة لاحقاً"
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr "عدد الطلاب"
......@@ -1823,6 +1836,8 @@ msgid ""
"Showing %(current_item_range)s out of %(total_items_count)s, sorted by "
"%(sort_name)s ascending"
msgstr ""
"إظهار %(current_item_range)s من أصل إجمالي يبلغ %(total_items_count)s، "
"مرتّبة وفقًا لـ %(sort_name)s تصاعدي"
#. Translators: sample result: "Showing 0-9 out of 25 total, sorted by Date
#. Added descending"
......@@ -1831,12 +1846,14 @@ msgid ""
"Showing %(current_item_range)s out of %(total_items_count)s, sorted by "
"%(sort_name)s descending"
msgstr ""
"إظهار %(current_item_range)s من أصل إجمالي يبلغ %(total_items_count)s، "
"مرتّبة وفقًا لـ %(sort_name)s تنازلي"
#. Translators: turns into "25 total" to be used in other sentences, e.g.
#. "Showing 0-9 out of 25 total".
#: cms/static/js/views/paging_header.js
msgid "%(total_items)s total"
msgstr ""
msgstr "مجموع يبلغ %(total_items)s "
#: cms/static/js/views/section_edit.js
msgid "Your change could not be saved"
......
......@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Azerbaijani (http://www.transifex.com/projects/p/edx-platform/language/az/)\n"
......@@ -1365,6 +1365,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/edx-platform/language/bg_BG/)\n"
......@@ -1365,6 +1365,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Bengali (Bangladesh) (http://www.transifex.com/projects/p/edx-platform/language/bn_BD/)\n"
......@@ -1366,6 +1366,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Bengali (India) (http://www.transifex.com/projects/p/edx-platform/language/bn_IN/)\n"
......@@ -1365,6 +1365,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Bosnian (http://www.transifex.com/projects/p/edx-platform/language/bs/)\n"
......@@ -1382,6 +1382,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -18,7 +18,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-12 18:15+0000\n"
"Last-Translator: mcolomer <mcmlilhity@gmail.com>\n"
"Language-Team: Catalan (http://www.transifex.com/projects/p/edx-platform/language/ca/)\n"
......@@ -1486,6 +1486,18 @@ msgstr ""
"No s'ha pogut obtenir les dades. Si us plau, intenta-ho més endavant."
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr "Nombre d'estudiants"
......
......@@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Catalan (Valencian) (http://www.transifex.com/projects/p/edx-platform/language/ca@valencia/)\n"
......@@ -1366,6 +1366,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -32,6 +32,7 @@ locales:
- gl # Galician
- he # Hebrew
- hi # Hindi
- hr # Croatian
- hu # Hungarian
- hy_AM # Armenian (Armenia)
- id # Indonesian
......
......@@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Czech (http://www.transifex.com/projects/p/edx-platform/language/cs/)\n"
......@@ -1388,6 +1388,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Welsh (http://www.transifex.com/projects/p/edx-platform/language/cy/)\n"
......@@ -1397,6 +1397,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Danish (http://www.transifex.com/projects/p/edx-platform/language/da/)\n"
......@@ -1365,6 +1365,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -28,7 +28,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: German (Germany) (http://www.transifex.com/projects/p/edx-platform/language/de_DE/)\n"
......@@ -1427,6 +1427,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr "Anzahl der Teilnehmer"
......
......@@ -17,7 +17,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: Greek (http://www.transifex.com/projects/p/edx-platform/language/el/)\n"
......@@ -1368,6 +1368,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
......@@ -16,7 +16,7 @@ msgid ""
msgstr ""
"Project-Id-Version: edx-platform\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-05-13 09:30-0400\n"
"POT-Creation-Date: 2014-05-19 08:16-0400\n"
"PO-Revision-Date: 2014-05-11 14:42+0000\n"
"Last-Translator: sarina <sarina@edx.org>\n"
"Language-Team: LOLCAT English (http://www.transifex.com/projects/p/edx-platform/language/en@lolcat/)\n"
......@@ -1412,6 +1412,18 @@ msgid "Unable to retrieve data, please try again later."
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "student(s) opened Subsection"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "students"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "questions"
msgstr ""
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
msgid "Number of Students"
msgstr ""
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment