diff --git a/AUTHORS b/AUTHORS index 5e22bf1b08..59ef640fbc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -233,4 +233,6 @@ Dongwook Yoon Awais Qureshi Eric Fischer Brian Beggs -Bill DeRusha \ No newline at end of file +Bill DeRusha +Kevin Falcone +Mirjam Škarica diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 743f54f829..fa6c7a31b3 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -67,20 +67,20 @@ def _click_advanced(): world.wait_for_visible(tab2_css) -def _find_matching_link(category, component_type): +def _find_matching_button(category, component_type): """ - Find the link with the specified text. There should be one and only one. + Find the button with the specified text. There should be one and only one. """ - # The tab shows links for the given category - links = world.css_find('div.new-component-{} a'.format(category)) + # The tab shows buttons for the given category + buttons = world.css_find('div.new-component-{} button'.format(category)) - # Find the link whose text matches what you're looking for - matched_links = [link for link in links if link.text == component_type] + # Find the button whose text matches what you're looking for + matched_buttons = [btn for btn in buttons if btn.text == component_type] # There should be one and only one - assert_equal(len(matched_links), 1) - return matched_links[0] + assert_equal(len(matched_buttons), 1) + return matched_buttons[0] def click_component_from_menu(category, component_type, is_advanced): @@ -100,7 +100,7 @@ def click_component_from_menu(category, component_type, is_advanced): # Retry this in case the list is empty because you tried too fast. link = world.retry_on_exception( - lambda: _find_matching_link(category, component_type), + lambda: _find_matching_button(category, component_type), ignored_exceptions=AssertionError ) diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index 16a6c88330..ec42635cc2 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -18,18 +18,6 @@ from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore -def print_out_all_courses(): - """ - Print out all the courses available in the course_key format so that - the user can correct any course_key mistakes - """ - courses = modulestore().get_courses_keys() - print 'Available courses:' - for course in courses: - print str(course) - print '' - - class Command(BaseCommand): """ Delete a MongoDB backed course @@ -58,8 +46,6 @@ class Command(BaseCommand): elif len(args) > 2: raise CommandError("Too many arguments! Expected ") - print_out_all_courses() - if not modulestore().get_course(course_key): raise CommandError("Course with '%s' key not found." % args[0]) @@ -67,4 +53,4 @@ class Command(BaseCommand): if query_yes_no("Deleting course {0}. Confirm?".format(course_key), default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"): delete_course_and_groups(course_key, ModuleStoreEnum.UserID.mgmt_command) - print_out_all_courses() + print "Deleted course {}".format(course_key) diff --git a/cms/djangoapps/contentstore/management/commands/reindex_course.py b/cms/djangoapps/contentstore/management/commands/reindex_course.py new file mode 100644 index 0000000000..56cb1008af --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/reindex_course.py @@ -0,0 +1,115 @@ +""" Management command to update courses' search index """ +import logging +from django.core.management import BaseCommand, CommandError +from optparse import make_option +from textwrap import dedent + +from contentstore.courseware_index import CoursewareSearchIndexer +from search.search_engine_base import SearchEngine +from elasticsearch import exceptions + +from opaque_keys.edx.keys import CourseKey +from opaque_keys import InvalidKeyError +from opaque_keys.edx.locator import CourseLocator + +from .prompt import query_yes_no + +from xmodule.modulestore.django import modulestore + + +class Command(BaseCommand): + """ + Command to re-index courses + + Examples: + + ./manage.py reindex_course - reindexes courses with keys course_id_1 and course_id_2 + ./manage.py reindex_course --all - reindexes all available courses + ./manage.py reindex_course --setup - reindexes all courses for devstack setup + """ + help = dedent(__doc__) + + can_import_settings = True + + args = "" + + all_option = make_option('--all', + action='store_true', + dest='all', + default=False, + help='Reindex all courses') + + setup_option = make_option('--setup', + action='store_true', + dest='setup', + default=False, + help='Reindex all courses on developers stack setup') + + option_list = BaseCommand.option_list + (all_option, setup_option) + + CONFIRMATION_PROMPT = u"Re-indexing all courses might be a time consuming operation. Do you want to continue?" + + def _parse_course_key(self, raw_value): + """ Parses course key from string """ + try: + result = CourseKey.from_string(raw_value) + except InvalidKeyError: + raise CommandError("Invalid course_key: '%s'." % raw_value) + + if not isinstance(result, CourseLocator): + raise CommandError(u"Argument {0} is not a course key".format(raw_value)) + + return result + + def handle(self, *args, **options): + """ + By convention set by Django developers, this method actually executes command's actions. + So, there could be no better docstring than emphasize this once again. + """ + all_option = options.get('all', False) + setup_option = options.get('setup', False) + index_all_courses_option = all_option or setup_option + + if len(args) == 0 and not index_all_courses_option: + raise CommandError(u"reindex_course requires one or more arguments: ") + + store = modulestore() + + if index_all_courses_option: + index_name = CoursewareSearchIndexer.INDEX_NAME + doc_type = CoursewareSearchIndexer.DOCUMENT_TYPE + if setup_option: + try: + # try getting the ElasticSearch engine + searcher = SearchEngine.get_search_engine(index_name) + except exceptions.ElasticsearchException as exc: + logging.exception('Search Engine error - %s', unicode(exc)) + return + + index_exists = searcher._es.indices.exists(index=index_name) # pylint: disable=protected-access + doc_type_exists = searcher._es.indices.exists_type( # pylint: disable=protected-access + index=index_name, + doc_type=doc_type + ) + + index_mapping = searcher._es.indices.get_mapping( # pylint: disable=protected-access + index=index_name, + doc_type=doc_type + ) if index_exists and doc_type_exists else {} + + if index_exists and index_mapping: + return + + # if reindexing is done during devstack setup step, don't prompt the user + if setup_option or query_yes_no(self.CONFIRMATION_PROMPT, default="no"): + # in case of --setup or --all, get the list of course keys from all courses + # that are stored in the modulestore + course_keys = [course.id for course in modulestore().get_courses()] + else: + return + else: + # in case course keys are provided as arguments + course_keys = map(self._parse_course_key, args) + + for course_key in course_keys: + CoursewareSearchIndexer.do_course_reindex(store, course_key) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py index 3cda8e29ea..d15698719d 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py @@ -5,6 +5,7 @@ Unittests for deleting a course in an chosen modulestore import unittest import mock +from opaque_keys.edx.locations import SlashSeparatedCourseKey from django.core.management import CommandError from contentstore.management.commands.delete_course import Command # pylint: disable=import-error from contentstore.tests.utils import CourseTestCase # pylint: disable=import-error @@ -94,27 +95,23 @@ class DeleteCourseTest(CourseTestCase): run=course_run ) - def test_courses_keys_listing(self): - """ - Test if the command lists out available course key courses - """ - courses = [str(key) for key in modulestore().get_courses_keys()] - self.assertIn("TestX/TS01/2015_Q1", courses) - def test_course_key_not_found(self): """ Test for when a non-existing course key is entered """ errstring = "Course with 'TestX/TS01/2015_Q7' key not found." with self.assertRaisesRegexp(CommandError, errstring): - self.command.handle("TestX/TS01/2015_Q7", "commit") + self.command.handle('TestX/TS01/2015_Q7', "commit") def test_course_deleted(self): """ Testing if the entered course was deleted """ + + #Test if the course that is about to be deleted exists + self.assertIsNotNone(modulestore().get_course(SlashSeparatedCourseKey("TestX", "TS01", "2015_Q1"))) + with mock.patch(self.YESNO_PATCH_LOCATION) as patched_yes_no: patched_yes_no.return_value = True - self.command.handle("TestX/TS01/2015_Q1", "commit") - courses = [unicode(key) for key in modulestore().get_courses_keys()] - self.assertNotIn("TestX/TS01/2015_Q1", courses) + self.command.handle('TestX/TS01/2015_Q1', "commit") + self.assertIsNone(modulestore().get_course(SlashSeparatedCourseKey("TestX", "TS01", "2015_Q1"))) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py new file mode 100644 index 0000000000..6383d046b8 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/tests/test_reindex_courses.py @@ -0,0 +1,131 @@ +""" Tests for course reindex command """ +import ddt +from django.core.management import call_command, CommandError +import mock + +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from common.test.utils import nostderr +from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory + +from contentstore.management.commands.reindex_course import Command as ReindexCommand +from contentstore.courseware_index import SearchIndexingError + + +@ddt.ddt +class TestReindexCourse(ModuleStoreTestCase): + """ Tests for course reindex command """ + def setUp(self): + """ Setup method - create courses """ + super(TestReindexCourse, self).setUp() + self.store = modulestore() + self.first_lib = LibraryFactory.create( + org="test", library="lib1", display_name="run1", default_store=ModuleStoreEnum.Type.split + ) + self.second_lib = LibraryFactory.create( + org="test", library="lib2", display_name="run2", default_store=ModuleStoreEnum.Type.split + ) + + self.first_course = CourseFactory.create( + org="test", course="course1", display_name="run1" + ) + self.second_course = CourseFactory.create( + org="test", course="course2", display_name="run1" + ) + + REINDEX_PATH_LOCATION = 'contentstore.management.commands.reindex_course.CoursewareSearchIndexer.do_course_reindex' + MODULESTORE_PATCH_LOCATION = 'contentstore.management.commands.reindex_course.modulestore' + YESNO_PATCH_LOCATION = 'contentstore.management.commands.reindex_course.query_yes_no' + + def _get_lib_key(self, library): + """ Get's library key as it is passed to indexer """ + return library.location.library_key + + def _build_calls(self, *courses): + """ Builds a list of mock.call instances representing calls to reindexing method """ + return [mock.call(self.store, course.id) for course in courses] + + def test_given_no_arguments_raises_command_error(self): + """ Test that raises CommandError for incorrect arguments """ + with self.assertRaises(SystemExit), nostderr(): + with self.assertRaisesRegexp(CommandError, ".* requires one or more arguments .*"): + call_command('reindex_course') + + @ddt.data('qwerty', 'invalid_key', 'xblock-v1:qwe+rty') + def test_given_invalid_course_key_raises_not_found(self, invalid_key): + """ Test that raises InvalidKeyError for invalid keys """ + errstring = "Invalid course_key: '%s'." % invalid_key + with self.assertRaises(SystemExit) as ex: + with self.assertRaisesRegexp(CommandError, errstring): + call_command('reindex_course', invalid_key) + self.assertEqual(ex.exception.code, 1) + + def test_given_library_key_raises_command_error(self): + """ Test that raises CommandError if library key is passed """ + with self.assertRaises(SystemExit), nostderr(): + with self.assertRaisesRegexp(SearchIndexingError, ".* is not a course key"): + call_command('reindex_course', unicode(self._get_lib_key(self.first_lib))) + + with self.assertRaises(SystemExit), nostderr(): + with self.assertRaisesRegexp(SearchIndexingError, ".* is not a course key"): + call_command('reindex_course', unicode(self._get_lib_key(self.second_lib))) + + with self.assertRaises(SystemExit), nostderr(): + with self.assertRaisesRegexp(SearchIndexingError, ".* is not a course key"): + call_command( + 'reindex_course', + unicode(self.second_course.id), + unicode(self._get_lib_key(self.first_lib)) + ) + + def test_given_id_list_indexes_courses(self): + """ Test that reindexes courses when given single course key or a list of course keys """ + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ + mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): + call_command('reindex_course', unicode(self.first_course.id)) + self.assertEqual(patched_index.mock_calls, self._build_calls(self.first_course)) + patched_index.reset_mock() + + call_command('reindex_course', unicode(self.second_course.id)) + self.assertEqual(patched_index.mock_calls, self._build_calls(self.second_course)) + patched_index.reset_mock() + + call_command( + 'reindex_course', + unicode(self.first_course.id), + unicode(self.second_course.id) + ) + expected_calls = self._build_calls(self.first_course, self.second_course) + self.assertEqual(patched_index.mock_calls, expected_calls) + + def test_given_all_key_prompts_and_reindexes_all_courses(self): + """ Test that reindexes all courses when --all key is given and confirmed """ + with mock.patch(self.YESNO_PATCH_LOCATION) as patched_yes_no: + patched_yes_no.return_value = True + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ + mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): + call_command('reindex_course', all=True) + + patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') + expected_calls = self._build_calls(self.first_course, self.second_course) + self.assertItemsEqual(patched_index.mock_calls, expected_calls) + + def test_given_all_key_prompts_and_reindexes_all_courses_cancelled(self): + """ Test that does not reindex anything when --all key is given and cancelled """ + with mock.patch(self.YESNO_PATCH_LOCATION) as patched_yes_no: + patched_yes_no.return_value = False + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index, \ + mock.patch(self.MODULESTORE_PATCH_LOCATION, mock.Mock(return_value=self.store)): + call_command('reindex_course', all=True) + + patched_yes_no.assert_called_once_with(ReindexCommand.CONFIRMATION_PROMPT, default='no') + patched_index.assert_not_called() + + def test_fail_fast_if_reindex_fails(self): + """ Test that fails on first reindexing exception """ + with mock.patch(self.REINDEX_PATH_LOCATION) as patched_index: + patched_index.side_effect = SearchIndexingError("message", []) + + with self.assertRaises(SearchIndexingError): + call_command('reindex_course', unicode(self.second_course.id)) diff --git a/cms/envs/common.py b/cms/envs/common.py index 25ae0c4250..fd73ab64d8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -170,6 +170,9 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': True, + # Teams search feature + 'ENABLE_TEAMS_SEARCH': False, + # Show video bumper in Studio 'ENABLE_VIDEO_BUMPER': False, diff --git a/cms/envs/test.py b/cms/envs/test.py index 16133ac8ef..1539c28fb7 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -281,5 +281,8 @@ SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine" # teams feature FEATURES['ENABLE_TEAMS'] = True +# teams search +FEATURES['ENABLE_TEAMS_SEARCH'] = True + # Dummy secret key for dev/test SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' diff --git a/cms/static/js/certificates/spec/views/certificate_details_spec.js b/cms/static/js/certificates/spec/views/certificate_details_spec.js index bf668213be..75c3f84d51 100644 --- a/cms/static/js/certificates/spec/views/certificate_details_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_details_spec.js @@ -79,7 +79,7 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails }; beforeEach(function() { - TemplateHelpers.installTemplates(['certificate-details', 'signatory-details', 'signatory-editor'], true); + TemplateHelpers.installTemplates(['certificate-details', 'signatory-details', 'signatory-editor', 'signatory-actions'], true); this.newModelOptions = {add: true}; this.model = new CertificateModel({ diff --git a/cms/static/js/certificates/views/signatory_details.js b/cms/static/js/certificates/views/signatory_details.js index 6d85898c51..1240f7b95c 100644 --- a/cms/static/js/certificates/views/signatory_details.js +++ b/cms/static/js/certificates/views/signatory_details.js @@ -40,6 +40,7 @@ function ($, _, str, Backbone, gettext, TemplateUtils, ViewUtils, BaseView, Sign eventAgg: this.eventAgg }); this.template = this.loadTemplate('signatory-details'); + this.signatory_action_template = this.loadTemplate('signatory-actions'); }, loadTemplate: function(name) { @@ -51,6 +52,7 @@ function ($, _, str, Backbone, gettext, TemplateUtils, ViewUtils, BaseView, Sign // Retrieve the edit view for this model if (event && event.preventDefault) { event.preventDefault(); } this.$el.html(this.edit_view.render()); + $(this.signatory_action_template()).appendTo(this.el); this.edit_view.delegateEvents(); this.delegateEvents(); }, diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index 749b49bd89..cb738ac959 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -552,7 +552,7 @@ define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/aja var clickNewComponent; clickNewComponent = function (index) { - containerPage.$(".new-component .new-component-type a.single-template")[index].click(); + containerPage.$(".new-component .new-component-type button.single-template")[index].click(); }; it('Attaches a handler to new component button', function() { @@ -598,7 +598,7 @@ define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/aja var showTemplatePicker, verifyCreateHtmlComponent; showTemplatePicker = function () { - containerPage.$('.new-component .new-component-type a.multiple-templates')[0].click(); + containerPage.$('.new-component .new-component-type button.multiple-templates')[0].click(); }; verifyCreateHtmlComponent = function (test, templateIndex, expectedRequest) { @@ -606,7 +606,7 @@ define(["jquery", "underscore", "underscore.string", "common/js/spec_helpers/aja renderContainerPage(test, mockContainerXBlockHtml); showTemplatePicker(); xblockCount = containerPage.$('.studio-xblock-wrapper').length; - containerPage.$('.new-component-html a')[templateIndex].click(); + containerPage.$('.new-component-html button')[templateIndex].click(); EditHelpers.verifyXBlockRequest(requests, expectedRequest); AjaxHelpers.respondWithJson(requests, {"locator": "new_item"}); respondWithHtml(mockXBlockHtml); diff --git a/cms/static/js/spec/views/pages/library_users_spec.js b/cms/static/js/spec/views/pages/library_users_spec.js index e92ece1e13..876a6fe30e 100644 --- a/cms/static/js/spec/views/pages/library_users_spec.js +++ b/cms/static/js/spec/views/pages/library_users_spec.js @@ -89,24 +89,24 @@ function ($, AjaxHelpers, ViewHelpers, ManageUsersFactory, ViewUtils) { it("displays an error when the user has already been added", function () { var requests = AjaxHelpers.requests(this); + var promptSpy = ViewHelpers.createPromptSpy(); $('.create-user-button').click(); $('.user-email-input').val('honor@example.com'); - var warningPromptSelector = '.wrapper-prompt.is-shown .prompt.warning'; - expect($(warningPromptSelector).length).toEqual(0); $('.form-create.create-user .action-primary').click(); - expect($(warningPromptSelector).length).toEqual(1); - expect($(warningPromptSelector)).toContainText('Already a library team member'); + ViewHelpers.verifyPromptShowing(promptSpy, 'Already a library team member'); expect(requests.length).toEqual(0); }); it("can remove a user's permission to access the library", function () { var requests = AjaxHelpers.requests(this); + var promptSpy = ViewHelpers.createPromptSpy(); var reloadSpy = spyOn(ViewUtils, 'reload'); var email = "honor@example.com"; $('.user-item[data-email="'+email+'"] .action-delete .delete').click(); - expect($('.wrapper-prompt.is-shown .prompt.warning').length).toEqual(1); - $('.wrapper-prompt.is-shown .action-primary').click(); + ViewHelpers.verifyPromptShowing(promptSpy, 'Are you sure?'); + ViewHelpers.confirmPrompt(promptSpy); + ViewHelpers.verifyPromptHidden(promptSpy); AjaxHelpers.expectJsonRequest(requests, 'DELETE', getUrl(email), {role: null}); AjaxHelpers.respondWithJson(requests, {'result': 'ok'}); expect(reloadSpy).toHaveBeenCalled(); diff --git a/cms/static/js/views/components/add_xblock.js b/cms/static/js/views/components/add_xblock.js index 8e3651eaff..6d92dd7494 100644 --- a/cms/static/js/views/components/add_xblock.js +++ b/cms/static/js/views/components/add_xblock.js @@ -6,10 +6,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/compo function ($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu) { var AddXBlockComponent = BaseView.extend({ events: { - 'click .new-component .new-component-type a.multiple-templates': 'showComponentTemplates', - 'click .new-component .new-component-type a.single-template': 'createNewComponent', + 'click .new-component .new-component-type button.multiple-templates': 'showComponentTemplates', + 'click .new-component .new-component-type button.single-template': 'createNewComponent', 'click .new-component .cancel-button': 'closeNewComponent', - 'click .new-component-templates .new-component-template a': 'createNewComponent', + 'click .new-component-templates .new-component-template .button-component': 'createNewComponent', 'click .new-component-templates .cancel-button': 'closeNewComponent' }, @@ -43,13 +43,17 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/compo type = $(event.currentTarget).data('type'); this.$('.new-component').slideUp(250); this.$('.new-component-' + type).slideDown(250); + this.$('.new-component-' + type + ' div').focus(); }, closeNewComponent: function(event) { event.preventDefault(); event.stopPropagation(); + type = $(event.currentTarget).data('type'); this.$('.new-component').slideDown(250); this.$('.new-component-templates').slideUp(250); + this.$('ul.new-component-type li button[data-type=' + type + ']').focus(); + }, createNewComponent: function(event) { diff --git a/cms/static/sass/assets/_graphics.scss b/cms/static/sass/assets/_graphics.scss index 8c9fab797d..9b0253b085 100644 --- a/cms/static/sass/assets/_graphics.scss +++ b/cms/static/sass/assets/_graphics.scss @@ -13,40 +13,35 @@ .large-advanced-icon { display: inline-block; - width: 100px; - height: 60px; - margin-right: ($baseline/4); + width: ($baseline*3); + height: ($baseline*3); background: url(../images/large-advanced-icon.png) center no-repeat; } .large-discussion-icon { display: inline-block; - width: 100px; - height: 60px; - margin-right: ($baseline/4); + width: ($baseline*3); + height: ($baseline*3); background: url(../images/large-discussion-icon.png) center no-repeat; } .large-html-icon { display: inline-block; - width: 100px; - height: 60px; - margin-right: ($baseline/4); + width: ($baseline*3); + height: ($baseline*3); background: url(../images/large-html-icon.png) center no-repeat; } .large-problem-icon { display: inline-block; - width: 100px; - height: 60px; - margin-right: ($baseline/4); + width: ($baseline*3); + height: ($baseline*3); background: url(../images/large-problem-icon.png) center no-repeat; } .large-video-icon { display: inline-block; - width: 100px; - height: 60px; - margin-right: ($baseline/4); + width: ($baseline*3); + height: ($baseline*3); background: url(../images/large-video-icon.png) center no-repeat; } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index 4af90d4d68..5f71f912aa 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -130,9 +130,10 @@ width: ($baseline*5); height: ($baseline*5); margin-bottom: ($baseline/2); + box-shadow: 0 1px 1px $shadow, 0 1px 0 rgba(255, 255, 255, .4) inset; border: 1px solid $green-d2; border-radius: ($baseline/4); - box-shadow: 0 1px 1px $shadow, 0 1px 0 rgba(255, 255, 255, .4) inset; + padding: 0; background-color: $green-l1; text-align: center; color: $white; @@ -195,16 +196,17 @@ } } - a { + .button-component { @include clearfix(); @include transition(none); @extend %t-demi-strong; display: block; + width: 100%; border: 0px; padding: 7px $baseline; background: $white; color: $gray-d3; - + text-align: left; &:hover { @include transition(background-color $tmg-f2 linear 0s); diff --git a/cms/static/sass/views/_certificates.scss b/cms/static/sass/views/_certificates.scss index 5c8578e769..ee0ad2205c 100644 --- a/cms/static/sass/views/_certificates.scss +++ b/cms/static/sass/views/_certificates.scss @@ -380,6 +380,12 @@ color: $gray-l3; } } + &.custom-signatory-action { + position: relative; + top: 0; + left: 0; + opacity: 1.0; + } } .copy { @@ -522,6 +528,10 @@ .signatory-panel-body { padding: $baseline; + + .signatory-image { + margin-top: 20px; + } } .signatory-panel-body label { diff --git a/cms/templates/certificates.html b/cms/templates/certificates.html index b6d13081c4..c39f994864 100644 --- a/cms/templates/certificates.html +++ b/cms/templates/certificates.html @@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _ <%block name="bodyclass">is-signedin course view-certificates <%block name="header_extras"> -% for template_name in ["certificate-details", "certificate-editor", "signatory-editor", "signatory-details", "basic-modal", "modal-button", "list", "upload-dialog", "certificate-web-preview"]: +% for template_name in ["certificate-details", "certificate-editor", "signatory-editor", "signatory-details", "basic-modal", "modal-button", "list", "upload-dialog", "certificate-web-preview", "signatory-actions"]: diff --git a/cms/templates/js/add-xblock-component-button.underscore b/cms/templates/js/add-xblock-component-button.underscore index 61da224f68..2418746be4 100644 --- a/cms/templates/js/add-xblock-component-button.underscore +++ b/cms/templates/js/add-xblock-component-button.underscore @@ -1,8 +1,9 @@ <% if (type === 'advanced' || templates.length > 1) { %> - + diff --git a/cms/templates/js/add-xblock-component-menu-problem.underscore b/cms/templates/js/add-xblock-component-menu-problem.underscore index 85bc58776b..40f3169d91 100644 --- a/cms/templates/js/add-xblock-component-menu-problem.underscore +++ b/cms/templates/js/add-xblock-component-menu-problem.underscore @@ -1,5 +1,11 @@ -
-
-<%= gettext("Cancel") %> diff --git a/cms/templates/js/add-xblock-component-menu.underscore b/cms/templates/js/add-xblock-component-menu.underscore index bf61436a20..f5f9760648 100644 --- a/cms/templates/js/add-xblock-component-menu.underscore +++ b/cms/templates/js/add-xblock-component-menu.underscore @@ -1,23 +1,29 @@ <% if (type === 'advanced' || templates.length > 1) { %> -
+ - <%= gettext("Cancel") %> <% } %> diff --git a/cms/templates/js/certificate-details.underscore b/cms/templates/js/certificate-details.underscore index 2d9ec0f3eb..32fdd3d28f 100644 --- a/cms/templates/js/certificate-details.underscore +++ b/cms/templates/js/certificate-details.underscore @@ -17,12 +17,12 @@ <%= gettext("Certificate Details") %>
- <%= gettext('Course Title') %>: + <%= gettext('Course Title') %>: <%= course.get('name') %>
<% if (course_title) { %>
- <%= gettext('Course Title Override') %>: + <%= gettext('Course Title Override') %>: <%= course_title %>
<% } %> diff --git a/cms/templates/js/certificate-editor.underscore b/cms/templates/js/certificate-editor.underscore index aff2a20010..13fecd3bed 100644 --- a/cms/templates/js/certificate-editor.underscore +++ b/cms/templates/js/certificate-editor.underscore @@ -19,6 +19,10 @@ <%= gettext("Description of the certificate") %>
+
+ + <%= course.get('name') %> +
" value="<%= course_title %>" aria-describedby="certificate-course-title-<%=uniqueId %>-tip" /> diff --git a/cms/templates/js/signatory-actions.underscore b/cms/templates/js/signatory-actions.underscore new file mode 100644 index 0000000000..e0c4ab60c5 --- /dev/null +++ b/cms/templates/js/signatory-actions.underscore @@ -0,0 +1,6 @@ +
+
+ + +
+
diff --git a/cms/templates/js/signatory-details.underscore b/cms/templates/js/signatory-details.underscore index 59bf7f217c..9487840207 100644 --- a/cms/templates/js/signatory-details.underscore +++ b/cms/templates/js/signatory-details.underscore @@ -9,16 +9,25 @@
Signatory <%= signatory_number %> 
- <%= gettext("Name") %>:  - <%= name %> +
+ <%= gettext("Name") %>:  + <%= name %> +
+
+ <%= gettext("Title") %>:  + <%= title %> +
+
+ <%= gettext("Organization") %>:  + <%= organization %> +
-
- <%= gettext("Title") %>:  - <%= title %> -
-
- <%= gettext("Organization") %>:  - <%= organization %> +
+ <% if (signature_image_path != "") { %> +
+ Signature Image +
+ <% } %>
diff --git a/cms/templates/js/signatory-editor.underscore b/cms/templates/js/signatory-editor.underscore index e146d023b3..7191373602 100644 --- a/cms/templates/js/signatory-editor.underscore +++ b/cms/templates/js/signatory-editor.underscore @@ -1,15 +1,5 @@
- - <% if (!is_editing_all_collections) { %> - - - <%= gettext("Close") %> - - - - <%= gettext("Save") %> - - <% } else if (signatories_count > 1 && (total_saved_signatories > 1 || isNew) ) { %> + <% if (is_editing_all_collections && signatories_count > 1 && (total_saved_signatories > 1 || isNew) ) { %> <%= gettext("Delete") %> diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index f5ce8a770a..4449ecaa6d 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -189,7 +189,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None): return { provider.provider_id: third_party_auth.pipeline.get_login_url( provider.provider_id, auth_entry, redirect_url=redirect_url - ) for provider in third_party_auth.provider.Registry.enabled() + ) for provider in third_party_auth.provider.Registry.accepting_logins() } diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index c65c7b3243..6f62e9e13b 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -1046,9 +1046,10 @@ class CourseEnrollment(models.Model): `course_key` is our usual course_id string (e.g. "edX/Test101/2013_Fall) `mode` is a string specifying what kind of enrollment this is. The - default is "honor", meaning honor certificate. Future options - may include "audit", "verified_id", etc. Please don't use it - until we have these mapped out. + default is 'honor', meaning honor certificate. Other options + include 'professional', 'verified', 'audit', + 'no-id-professional' and 'credit'. + See CourseMode in common/djangoapps/course_modes/models.py. `check_access`: if True, we check that an accessible course actually exists for the given course_key before we enroll the student. diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 624a945f96..0f262df3cc 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -447,10 +447,11 @@ def register_user(request, extra_context=None): if third_party_auth.is_enabled() and pipeline.running(request): running_pipeline = pipeline.get(request) current_provider = provider.Registry.get_from_pipeline(running_pipeline) - overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs')) - overrides['running_pipeline'] = running_pipeline - overrides['selected_provider'] = current_provider.name - context.update(overrides) + if current_provider is not None: + overrides = current_provider.get_register_form_data(running_pipeline.get('kwargs')) + overrides['running_pipeline'] = running_pipeline + overrides['selected_provider'] = current_provider.name + context.update(overrides) return render_to_response('register.html', context) @@ -1769,6 +1770,10 @@ def auto_auth(request): full_name = request.GET.get('full_name', username) is_staff = request.GET.get('staff', None) course_id = request.GET.get('course_id', None) + + # mode has to be one of 'honor'/'professional'/'verified'/'audit'/'no-id-professional'/'credit' + enrollment_mode = request.GET.get('enrollment_mode', 'honor') + course_key = None if course_id: course_key = CourseLocator.from_string(course_id) @@ -1816,7 +1821,7 @@ def auto_auth(request): # Enroll the user in a course if course_key is not None: - CourseEnrollment.enroll(user, course_key) + CourseEnrollment.enroll(user, course_key, mode=enrollment_mode) # Apply the roles for role_name in role_names: diff --git a/common/djangoapps/third_party_auth/admin.py b/common/djangoapps/third_party_auth/admin.py index a949f3fcb0..6b2a9bfccb 100644 --- a/common/djangoapps/third_party_auth/admin.py +++ b/common/djangoapps/third_party_auth/admin.py @@ -6,7 +6,7 @@ Admin site configuration for third party authentication from django.contrib import admin from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin -from .models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, SAMLProviderData +from .models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, SAMLProviderData, LTIProviderConfig from .tasks import fetch_saml_metadata @@ -88,3 +88,26 @@ class SAMLProviderDataAdmin(admin.ModelAdmin): return self.readonly_fields admin.site.register(SAMLProviderData, SAMLProviderDataAdmin) + + +class LTIProviderConfigAdmin(KeyedConfigurationModelAdmin): + """ Django Admin class for LTIProviderConfig """ + + exclude = ( + 'icon_class', + 'secondary', + ) + + def get_list_display(self, request): + """ Don't show every single field in the admin change list """ + return ( + 'name', + 'enabled', + 'lti_consumer_key', + 'lti_max_timestamp_age', + 'change_date', + 'changed_by', + 'edit_link', + ) + +admin.site.register(LTIProviderConfig, LTIProviderConfigAdmin) diff --git a/common/djangoapps/third_party_auth/lti.py b/common/djangoapps/third_party_auth/lti.py new file mode 100644 index 0000000000..222f76a388 --- /dev/null +++ b/common/djangoapps/third_party_auth/lti.py @@ -0,0 +1,202 @@ +""" +Third-party-auth module for Learning Tools Interoperability +""" +import logging +import calendar +import time + +from django.contrib.auth import REDIRECT_FIELD_NAME +from oauthlib.common import Request +from oauthlib.oauth1.rfc5849.signature import ( + normalize_base_string_uri, + normalize_parameters, + collect_parameters, + construct_base_string, + sign_hmac_sha1, +) +from social.backends.base import BaseAuth +from social.exceptions import AuthFailed +from social.utils import sanitize_redirect + +log = logging.getLogger(__name__) + +LTI_PARAMS_KEY = 'tpa-lti-params' + + +class LTIAuthBackend(BaseAuth): + """ + Third-party-auth module for Learning Tools Interoperability + """ + + name = 'lti' + + def start(self): + """ + Prepare to handle a login request. + + This method replaces social.actions.do_auth and must be kept in sync + with any upstream changes in that method. In the current version of + the upstream, this means replacing the logic to populate the session + from request parameters, and not calling backend.start() to avoid + an unwanted redirect to the non-existent login page. + """ + + # Clean any partial pipeline data + self.strategy.clean_partial_pipeline() + + # Save validated LTI parameters (or None if invalid or not submitted) + validated_lti_params = self.get_validated_lti_params(self.strategy) + + # Set a auth_entry here so we don't have to receive that as a custom parameter + self.strategy.session_setdefault('auth_entry', 'login') + + if not validated_lti_params: + self.strategy.session_set(LTI_PARAMS_KEY, None) + raise AuthFailed(self, "LTI parameters could not be validated.") + else: + self.strategy.session_set(LTI_PARAMS_KEY, validated_lti_params) + + # Save extra data into session. + # While Basic LTI 1.0 specifies that the message is to be signed using OAuth, implying + # that any GET parameters should be stripped from the base URL and included as signed + # parameters, typical LTI Tool Consumer implementations do not support this behaviour. As + # a workaround, we accept TPA parameters from LTI custom parameters prefixed with "tpa_". + + for field_name in self.setting('FIELDS_STORED_IN_SESSION', []): + if 'custom_tpa_' + field_name in validated_lti_params: + self.strategy.session_set(field_name, validated_lti_params['custom_tpa_' + field_name]) + + if 'custom_tpa_' + REDIRECT_FIELD_NAME in validated_lti_params: + # Check and sanitize a user-defined GET/POST next field value + redirect_uri = validated_lti_params['custom_tpa_' + REDIRECT_FIELD_NAME] + if self.setting('SANITIZE_REDIRECTS', True): + redirect_uri = sanitize_redirect(self.strategy.request_host(), redirect_uri) + self.strategy.session_set(REDIRECT_FIELD_NAME, redirect_uri or self.setting('LOGIN_REDIRECT_URL')) + + def auth_html(self): + """ + Not used + """ + raise NotImplementedError("Not used") + + def auth_url(self): + """ + Not used + """ + raise NotImplementedError("Not used") + + def auth_complete(self, *args, **kwargs): + """ + Completes third-part-auth authentication + """ + lti_params = self.strategy.session_get(LTI_PARAMS_KEY) + kwargs.update({'response': {LTI_PARAMS_KEY: lti_params}, 'backend': self}) + return self.strategy.authenticate(*args, **kwargs) + + def get_user_id(self, details, response): + """ + Computes social auth username from LTI parameters + """ + lti_params = response[LTI_PARAMS_KEY] + return lti_params['oauth_consumer_key'] + ":" + lti_params['user_id'] + + def get_user_details(self, response): + """ + Retrieves user details from LTI parameters + """ + details = {} + lti_params = response[LTI_PARAMS_KEY] + + def add_if_exists(lti_key, details_key): + """ + Adds LTI parameter to user details dict if it exists + """ + if lti_key in lti_params and lti_params[lti_key]: + details[details_key] = lti_params[lti_key] + + add_if_exists('email', 'email') + add_if_exists('lis_person_name_full', 'fullname') + add_if_exists('lis_person_name_given', 'first_name') + add_if_exists('lis_person_name_family', 'last_name') + return details + + @classmethod + def get_validated_lti_params(cls, strategy): + """ + Validates LTI signature and returns LTI parameters + """ + request = Request( + uri=strategy.request.build_absolute_uri(), http_method=strategy.request.method, body=strategy.request.body + ) + lti_consumer_key = request.oauth_consumer_key + (lti_consumer_valid, lti_consumer_secret, lti_max_timestamp_age) = cls.load_lti_consumer(lti_consumer_key) + current_time = calendar.timegm(time.gmtime()) + return cls._get_validated_lti_params_from_values( + request=request, current_time=current_time, + lti_consumer_valid=lti_consumer_valid, + lti_consumer_secret=lti_consumer_secret, + lti_max_timestamp_age=lti_max_timestamp_age + ) + + @classmethod + def _get_validated_lti_params_from_values(cls, request, current_time, + lti_consumer_valid, lti_consumer_secret, lti_max_timestamp_age): + """ + Validates LTI signature and returns LTI parameters + """ + + # Taking a cue from oauthlib, to avoid leaking information through a timing attack, + # we proceed through the entire validation before rejecting any request for any reason. + # However, as noted there, the value of doing this is dubious. + + base_uri = normalize_base_string_uri(request.uri) + parameters = collect_parameters(uri_query=request.uri_query, body=request.body) + parameters_string = normalize_parameters(parameters) + base_string = construct_base_string(request.http_method, base_uri, parameters_string) + + computed_signature = sign_hmac_sha1(base_string, unicode(lti_consumer_secret), '') + submitted_signature = request.oauth_signature + + data = {parameter_value_pair[0]: parameter_value_pair[1] for parameter_value_pair in parameters} + + def safe_int(value): + """ + Interprets parameter as an int or returns 0 if not possible + """ + try: + return int(value) + except (ValueError, TypeError): + return 0 + + oauth_timestamp = safe_int(request.oauth_timestamp) + + # As this must take constant time, do not use shortcutting operators such as 'and'. + # Instead, use constant time operators such as '&', which is the bitwise and. + valid = (lti_consumer_valid) + valid = valid & (submitted_signature == computed_signature) + valid = valid & (request.oauth_version == '1.0') + valid = valid & (request.oauth_signature_method == 'HMAC-SHA1') + valid = valid & ('user_id' in data) # Not required by LTI but can't log in without one + valid = valid & (oauth_timestamp >= current_time - lti_max_timestamp_age) + valid = valid & (oauth_timestamp <= current_time) + + if valid: + return data + else: + return None + + @classmethod + def load_lti_consumer(cls, lti_consumer_key): + """ + Retrieves LTI consumer details from database + """ + from .models import LTIProviderConfig + provider_config = LTIProviderConfig.current(lti_consumer_key) + if provider_config and provider_config.enabled: + return ( + provider_config.enabled, + provider_config.get_lti_consumer_secret(), + provider_config.lti_max_timestamp_age, + ) + else: + return False, '', -1 diff --git a/common/djangoapps/third_party_auth/migrations/0004_lti_tool_consumers.py b/common/djangoapps/third_party_auth/migrations/0004_lti_tool_consumers.py new file mode 100644 index 0000000000..dfef06ae73 --- /dev/null +++ b/common/djangoapps/third_party_auth/migrations/0004_lti_tool_consumers.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# pylint: disable=C,E,F,R,W +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'LTIProviderConfig' + db.create_table('third_party_auth_ltiproviderconfig', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('icon_class', self.gf('django.db.models.fields.CharField')(default='fa-sign-in', max_length=50)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('secondary', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('skip_registration_form', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('skip_email_verification', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('lti_consumer_key', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('lti_consumer_secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('lti_max_timestamp_age', self.gf('django.db.models.fields.IntegerField')(default=10)), + )) + db.send_create_signal('third_party_auth', ['LTIProviderConfig']) + + + def backwards(self, orm): + # Deleting model 'LTIProviderConfig' + db.delete_table('third_party_auth_ltiproviderconfig') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'third_party_auth.ltiproviderconfig': { + 'Meta': {'object_name': 'LTIProviderConfig'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lti_consumer_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'lti_consumer_secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'lti_max_timestamp_age': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'third_party_auth.oauth2providerconfig': { + 'Meta': {'object_name': 'OAuth2ProviderConfig'}, + 'backend_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'secret': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'third_party_auth.samlconfiguration': { + 'Meta': {'object_name': 'SAMLConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'default': "'http://saml.example.com'", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org_info_str': ('django.db.models.fields.TextField', [], {'default': '\'{"en-US": {"url": "http://www.example.com", "displayname": "Example Inc.", "name": "example"}}\''}), + 'other_config_str': ('django.db.models.fields.TextField', [], {'default': '\'{\\n"SECURITY_CONFIG": {"metadataCacheDuration": 604800, "signMetadata": false}\\n}\''}), + 'private_key': ('django.db.models.fields.TextField', [], {}), + 'public_key': ('django.db.models.fields.TextField', [], {}) + }, + 'third_party_auth.samlproviderconfig': { + 'Meta': {'object_name': 'SAMLProviderConfig'}, + 'attr_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_first_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_full_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_last_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_user_permanent_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'attr_username': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'backend_name': ('django.db.models.fields.CharField', [], {'default': "'tpa-saml'", 'max_length': '50'}), + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'icon_class': ('django.db.models.fields.CharField', [], {'default': "'fa-sign-in'", 'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idp_slug': ('django.db.models.fields.SlugField', [], {'max_length': '30'}), + 'metadata_source': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'other_settings': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'secondary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_email_verification': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'skip_registration_form': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'third_party_auth.samlproviderdata': { + 'Meta': {'ordering': "('-fetched_at',)", 'object_name': 'SAMLProviderData'}, + 'entity_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'expires_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'fetched_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'public_key': ('django.db.models.fields.TextField', [], {}), + 'sso_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + } + } + + complete_apps = ['third_party_auth'] diff --git a/common/djangoapps/third_party_auth/models.py b/common/djangoapps/third_party_auth/models.py index 79ba5746bf..253e1e610b 100644 --- a/common/djangoapps/third_party_auth/models.py +++ b/common/djangoapps/third_party_auth/models.py @@ -3,6 +3,8 @@ Models used to implement SAML SSO support in third_party_auth (inlcuding Shibboleth support) """ +from __future__ import absolute_import + from config_models.models import ConfigurationModel, cache from django.conf import settings from django.core.exceptions import ValidationError @@ -11,9 +13,11 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ import json import logging +from provider.utils import long_token from social.backends.base import BaseAuth from social.backends.oauth import OAuthAuth from social.backends.saml import SAMLAuth, SAMLIdentityProvider +from .lti import LTIAuthBackend, LTI_PARAMS_KEY from social.exceptions import SocialAuthBaseException from social.utils import module_member @@ -32,6 +36,7 @@ def _load_backend_classes(base_class=BaseAuth): _PSA_BACKENDS = {backend_class.name: backend_class for backend_class in _load_backend_classes()} _PSA_OAUTH2_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(OAuthAuth)] _PSA_SAML_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(SAMLAuth)] +_LTI_BACKENDS = [backend_class.name for backend_class in _load_backend_classes(LTIAuthBackend)] def clean_json(value, of_type): @@ -95,6 +100,7 @@ class ProviderConfig(ConfigurationModel): ) prefix = None # used for provider_id. Set to a string value in subclass backend_name = None # Set to a field or fixed value in subclass + accepts_logins = True # Whether to display a sign-in button when the provider is enabled # "enabled" field is inherited from ConfigurationModel @@ -454,3 +460,70 @@ class SAMLProviderData(models.Model): cache.set(cls.cache_key_name(entity_id), current, cls.cache_timeout) return current + + +class LTIProviderConfig(ProviderConfig): + """ + Configuration required for this edX instance to act as a LTI + Tool Provider and allow users to authenticate and be enrolled in a + course via third party LTI Tool Consumers. + """ + prefix = 'lti' + backend_name = 'lti' + icon_class = None # This provider is not visible to users + secondary = False # This provider is not visible to users + accepts_logins = False # LTI login cannot be initiated by the tool provider + KEY_FIELDS = ('lti_consumer_key', ) + + lti_consumer_key = models.CharField( + max_length=255, + help_text=( + 'The name that the LTI Tool Consumer will use to identify itself' + ) + ) + lti_consumer_secret = models.CharField( + default=long_token, + max_length=255, + help_text=( + 'The shared secret that the LTI Tool Consumer will use to ' + 'authenticate requests. Only this edX instance and this ' + 'tool consumer instance should know this value. ' + 'For increased security, you can avoid storing this in ' + 'your database by leaving this field blank and setting ' + 'SOCIAL_AUTH_LTI_CONSUMER_SECRETS = {"consumer key": "secret", ...} ' + 'in your instance\'s Django setttigs (or lms.auth.json)' + ), + blank=True, + ) + + lti_max_timestamp_age = models.IntegerField( + default=10, + help_text=( + 'The maximum age of oauth_timestamp values, in seconds.' + ) + ) + + def match_social_auth(self, social_auth): + """ Is this provider being used for this UserSocialAuth entry? """ + prefix = self.lti_consumer_key + ":" + return self.backend_name == social_auth.provider and social_auth.uid.startswith(prefix) + + def is_active_for_pipeline(self, pipeline): + """ Is this provider being used for the specified pipeline? """ + try: + return ( + self.backend_name == pipeline['backend'] and + self.lti_consumer_key == pipeline['kwargs']['response'][LTI_PARAMS_KEY]['oauth_consumer_key'] + ) + except KeyError: + return False + + def get_lti_consumer_secret(self): + """ If the LTI consumer secret is not stored in the database, check Django settings instead """ + if self.lti_consumer_secret: + return self.lti_consumer_secret + return getattr(settings, 'SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}).get(self.lti_consumer_key, '') + + class Meta(object): # pylint: disable=missing-docstring + verbose_name = "Provider Configuration (LTI)" + verbose_name_plural = verbose_name diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 866f66e4f3..ba7da7ebec 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -99,13 +99,6 @@ AUTH_ENTRY_LOGIN = 'login' AUTH_ENTRY_REGISTER = 'register' AUTH_ENTRY_ACCOUNT_SETTINGS = 'account_settings' -# This is left-over from an A/B test -# of the new combined login/registration page (ECOM-369) -# We need to keep both the old and new entry points -# until every session from before the test ended has expired. -AUTH_ENTRY_LOGIN_2 = 'account_login' -AUTH_ENTRY_REGISTER_2 = 'account_register' - # Entry modes into the authentication process by a remote API call (as opposed to a browser session). AUTH_ENTRY_LOGIN_API = 'login_api' AUTH_ENTRY_REGISTER_API = 'register_api' @@ -126,28 +119,12 @@ AUTH_DISPATCH_URLS = { AUTH_ENTRY_LOGIN: '/login', AUTH_ENTRY_REGISTER: '/register', AUTH_ENTRY_ACCOUNT_SETTINGS: '/account/settings', - - # This is left-over from an A/B test - # of the new combined login/registration page (ECOM-369) - # We need to keep both the old and new entry points - # until every session from before the test ended has expired. - AUTH_ENTRY_LOGIN_2: '/account/login/', - AUTH_ENTRY_REGISTER_2: '/account/register/', - } _AUTH_ENTRY_CHOICES = frozenset([ AUTH_ENTRY_LOGIN, AUTH_ENTRY_REGISTER, AUTH_ENTRY_ACCOUNT_SETTINGS, - - # This is left-over from an A/B test - # of the new combined login/registration page (ECOM-369) - # We need to keep both the old and new entry points - # until every session from before the test ended has expired. - AUTH_ENTRY_LOGIN_2, - AUTH_ENTRY_REGISTER_2, - AUTH_ENTRY_LOGIN_API, AUTH_ENTRY_REGISTER_API, ]) @@ -395,9 +372,10 @@ def get_provider_user_states(user): if enabled_provider.match_social_auth(auth): association_id = auth.id break - states.append( - ProviderUserState(enabled_provider, user, association_id) - ) + if enabled_provider.accepts_logins or association_id: + states.append( + ProviderUserState(enabled_provider, user, association_id) + ) return states @@ -508,13 +486,13 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia if not user: if auth_entry in [AUTH_ENTRY_LOGIN_API, AUTH_ENTRY_REGISTER_API]: return HttpResponseBadRequest() - elif auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]: + elif auth_entry == AUTH_ENTRY_LOGIN: # User has authenticated with the third party provider but we don't know which edX # account corresponds to them yet, if any. if should_force_account_creation(): return dispatch_to_register() return dispatch_to_login() - elif auth_entry in [AUTH_ENTRY_REGISTER, AUTH_ENTRY_REGISTER_2]: + elif auth_entry == AUTH_ENTRY_REGISTER: # User has authenticated with the third party provider and now wants to finish # creating their edX account. return dispatch_to_register() @@ -603,7 +581,7 @@ def login_analytics(strategy, auth_entry, *args, **kwargs): """ Sends login info to Segment.io """ event_name = None - if auth_entry in [AUTH_ENTRY_LOGIN, AUTH_ENTRY_LOGIN_2]: + if auth_entry == AUTH_ENTRY_LOGIN: event_name = 'edx.bi.user.account.authenticated' elif auth_entry in [AUTH_ENTRY_ACCOUNT_SETTINGS]: event_name = 'edx.bi.user.account.linked' diff --git a/common/djangoapps/third_party_auth/provider.py b/common/djangoapps/third_party_auth/provider.py index 415e670900..dde2b41608 100644 --- a/common/djangoapps/third_party_auth/provider.py +++ b/common/djangoapps/third_party_auth/provider.py @@ -2,8 +2,8 @@ Third-party auth provider configuration API. """ from .models import ( - OAuth2ProviderConfig, SAMLConfiguration, SAMLProviderConfig, - _PSA_OAUTH2_BACKENDS, _PSA_SAML_BACKENDS + OAuth2ProviderConfig, SAMLConfiguration, SAMLProviderConfig, LTIProviderConfig, + _PSA_OAUTH2_BACKENDS, _PSA_SAML_BACKENDS, _LTI_BACKENDS, ) @@ -26,12 +26,21 @@ class Registry(object): provider = SAMLProviderConfig.current(idp_slug) if provider.enabled and provider.backend_name in _PSA_SAML_BACKENDS: yield provider + for consumer_key in LTIProviderConfig.key_values('lti_consumer_key', flat=True): + provider = LTIProviderConfig.current(consumer_key) + if provider.enabled and provider.backend_name in _LTI_BACKENDS: + yield provider @classmethod def enabled(cls): """Returns list of enabled providers.""" return sorted(cls._enabled_providers(), key=lambda provider: provider.name) + @classmethod + def accepting_logins(cls): + """Returns list of providers that can be used to initiate logins currently""" + return [provider for provider in cls.enabled() if provider.accepts_logins] + @classmethod def get(cls, provider_id): """Gets provider by provider_id string if enabled, else None.""" @@ -83,3 +92,8 @@ class Registry(object): provider = SAMLProviderConfig.current(idp_name) if provider.backend_name == backend_name and provider.enabled: yield provider + elif backend_name in _LTI_BACKENDS: + for consumer_key in LTIProviderConfig.key_values('lti_consumer_key', flat=True): + provider = LTIProviderConfig.current(consumer_key) + if provider.backend_name == backend_name and provider.enabled: + yield provider diff --git a/common/djangoapps/third_party_auth/strategy.py b/common/djangoapps/third_party_auth/strategy.py index eeff362ff1..fbb6c765b3 100644 --- a/common/djangoapps/third_party_auth/strategy.py +++ b/common/djangoapps/third_party_auth/strategy.py @@ -20,6 +20,8 @@ class ConfigurationModelStrategy(DjangoStrategy): OAuthAuth subclasses will call this method for every setting they want to look up. SAMLAuthBackend subclasses will call this method only after first checking if the setting 'name' is configured via SAMLProviderConfig. + LTIAuthBackend subclasses will call this method only after first checking if the + setting 'name' is configured via LTIProviderConfig. """ if isinstance(backend, OAuthAuth): provider_config = OAuth2ProviderConfig.current(backend.name) @@ -29,6 +31,6 @@ class ConfigurationModelStrategy(DjangoStrategy): return provider_config.get_setting(name) except KeyError: pass - # At this point, we know 'name' is not set in a [OAuth2|SAML]ProviderConfig row. + # At this point, we know 'name' is not set in a [OAuth2|LTI|SAML]ProviderConfig row. # It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION': return super(ConfigurationModelStrategy, self).setting(name, default, backend) diff --git a/common/djangoapps/third_party_auth/tests/data/lti_cannot_add_get_params.txt b/common/djangoapps/third_party_auth/tests/data/lti_cannot_add_get_params.txt new file mode 100644 index 0000000000..3117b3163c --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_cannot_add_get_params.txt @@ -0,0 +1 @@ +lti_message_type=basic-lti-launch-request<i_version=LTI-1p0&lis_outcome_service_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Fcommon%2Ftool_consumer_outcome.php%3Fb64%3DMTIzNDU6OjpzZWNyZXQ%3D&lis_result_sourcedid=feb-123-456-2929%3A%3A28883&launch_presentation_return_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Flms_return.php&user_id=292832126&custom_extra=parameter&oauth_version=1.0&oauth_nonce=c4936a7122f4f85c2d95afe32391573b&oauth_timestamp=1436823553&oauth_consumer_key=12345&oauth_signature_method=HMAC-SHA1&oauth_signature=STPWUouDw%2FlRGD4giWf8lpGTc54%3D&oauth_callback=about%3Ablank \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/data/lti_garbage.txt b/common/djangoapps/third_party_auth/tests/data/lti_garbage.txt new file mode 100644 index 0000000000..791be54782 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_garbage.txt @@ -0,0 +1 @@ +some=garbage&values=provided \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/data/lti_invalid_signature.txt b/common/djangoapps/third_party_auth/tests/data/lti_invalid_signature.txt new file mode 100644 index 0000000000..9cac2ecbd1 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_invalid_signature.txt @@ -0,0 +1 @@ +lti_message_type=basic-lti-launch-request<i_version=LTI-1p0&lis_outcome_service_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Fcommon%2Ftool_consumer_outcome.php%3Fb64%3DMTIzNDU6OjpzZWNyZXQ%3D&lis_result_sourcedid=feb-123-456-2929%3A%3A28883&launch_presentation_return_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Flms_return.php&user_id=292832126&custom_extra=parameter&oauth_version=1.0&oauth_nonce=c4936a7122f4f85c2d95afe32391573b&oauth_timestamp=1436823553&oauth_consumer_key=12345&oauth_signature_method=HMAC-SHA1&oauth_signature=STPWUouDw%2FlRGD4giWf8lpXXXXX%3D&oauth_callback=about%3Ablank \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/data/lti_old_timestamp.txt b/common/djangoapps/third_party_auth/tests/data/lti_old_timestamp.txt new file mode 100644 index 0000000000..3117b3163c --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_old_timestamp.txt @@ -0,0 +1 @@ +lti_message_type=basic-lti-launch-request<i_version=LTI-1p0&lis_outcome_service_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Fcommon%2Ftool_consumer_outcome.php%3Fb64%3DMTIzNDU6OjpzZWNyZXQ%3D&lis_result_sourcedid=feb-123-456-2929%3A%3A28883&launch_presentation_return_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Flms_return.php&user_id=292832126&custom_extra=parameter&oauth_version=1.0&oauth_nonce=c4936a7122f4f85c2d95afe32391573b&oauth_timestamp=1436823553&oauth_consumer_key=12345&oauth_signature_method=HMAC-SHA1&oauth_signature=STPWUouDw%2FlRGD4giWf8lpGTc54%3D&oauth_callback=about%3Ablank \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/data/lti_valid_request.txt b/common/djangoapps/third_party_auth/tests/data/lti_valid_request.txt new file mode 100644 index 0000000000..3117b3163c --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_valid_request.txt @@ -0,0 +1 @@ +lti_message_type=basic-lti-launch-request<i_version=LTI-1p0&lis_outcome_service_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Fcommon%2Ftool_consumer_outcome.php%3Fb64%3DMTIzNDU6OjpzZWNyZXQ%3D&lis_result_sourcedid=feb-123-456-2929%3A%3A28883&launch_presentation_return_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Flms_return.php&user_id=292832126&custom_extra=parameter&oauth_version=1.0&oauth_nonce=c4936a7122f4f85c2d95afe32391573b&oauth_timestamp=1436823553&oauth_consumer_key=12345&oauth_signature_method=HMAC-SHA1&oauth_signature=STPWUouDw%2FlRGD4giWf8lpGTc54%3D&oauth_callback=about%3Ablank \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/data/lti_valid_request_with_get_params.txt b/common/djangoapps/third_party_auth/tests/data/lti_valid_request_with_get_params.txt new file mode 100644 index 0000000000..673dadbfc4 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/data/lti_valid_request_with_get_params.txt @@ -0,0 +1 @@ +lti_message_type=basic-lti-launch-request&lis_outcome_service_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Fcommon%2Ftool_consumer_outcome.php%3Fb64%3DMTIzNDU6OjpzZWNyZXQ%3D&lis_result_sourcedid=feb-123-456-2929%3A%3A28883&launch_presentation_return_url=http%3A%2F%2Fwww.imsglobal.org%2Fdevelopers%2FLTI%2Ftest%2Fv1p1%2Flms_return.php&custom_extra=parameter&oauth_version=1.0&oauth_nonce=c4936a7122f4f85c2d95afe32391573b&oauth_timestamp=1436823553&oauth_consumer_key=12345&oauth_signature_method=HMAC-SHA1&oauth_signature=STPWUouDw%2FlRGD4giWf8lpGTc54%3D&oauth_callback=about%3Ablank \ No newline at end of file diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index f1bed2ef5a..3b69b1340c 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -381,12 +381,6 @@ class IntegrationTest(testutil.TestCase, test.TestCase): def test_canceling_authentication_redirects_to_register_when_auth_entry_register(self): self.assert_exception_redirect_looks_correct('/register', auth_entry=pipeline.AUTH_ENTRY_REGISTER) - def test_canceling_authentication_redirects_to_login_when_auth_login_2(self): - self.assert_exception_redirect_looks_correct('/account/login/', auth_entry=pipeline.AUTH_ENTRY_LOGIN_2) - - def test_canceling_authentication_redirects_to_login_when_auth_register_2(self): - self.assert_exception_redirect_looks_correct('/account/register/', auth_entry=pipeline.AUTH_ENTRY_REGISTER_2) - def test_canceling_authentication_redirects_to_account_settings_when_auth_entry_account_settings(self): self.assert_exception_redirect_looks_correct( '/account/settings', auth_entry=pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS diff --git a/common/djangoapps/third_party_auth/tests/specs/test_lti.py b/common/djangoapps/third_party_auth/tests/specs/test_lti.py new file mode 100644 index 0000000000..fa9e2398e4 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/specs/test_lti.py @@ -0,0 +1,159 @@ +""" +Integration tests for third_party_auth LTI auth providers +""" +import unittest +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from oauthlib.oauth1.rfc5849 import Client, SIGNATURE_TYPE_BODY +from third_party_auth.tests import testutil + +FORM_ENCODED = 'application/x-www-form-urlencoded' + +LTI_CONSUMER_KEY = 'consumer' +LTI_CONSUMER_SECRET = 'secret' +LTI_TPA_LOGIN_URL = 'http://testserver/auth/login/lti/' +LTI_TPA_COMPLETE_URL = 'http://testserver/auth/complete/lti/' +OTHER_LTI_CONSUMER_KEY = 'settings-consumer' +OTHER_LTI_CONSUMER_SECRET = 'secret2' +LTI_USER_ID = 'lti_user_id' +EDX_USER_ID = 'test_user' +EMAIL = 'lti_user@example.com' + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class IntegrationTestLTI(testutil.TestCase): + """ + Integration tests for third_party_auth LTI auth providers + """ + + def setUp(self): + super(IntegrationTestLTI, self).setUp() + self.configure_lti_provider( + name='Other Tool Consumer 1', enabled=True, + lti_consumer_key='other1', + lti_consumer_secret='secret1', + lti_max_timestamp_age=10, + ) + self.configure_lti_provider( + name='LTI Test Tool Consumer', enabled=True, + lti_consumer_key=LTI_CONSUMER_KEY, + lti_consumer_secret=LTI_CONSUMER_SECRET, + lti_max_timestamp_age=10, + ) + self.configure_lti_provider( + name='Tool Consumer with Secret in Settings', enabled=True, + lti_consumer_key=OTHER_LTI_CONSUMER_KEY, + lti_consumer_secret='', + lti_max_timestamp_age=10, + ) + self.lti = Client( + client_key=LTI_CONSUMER_KEY, + client_secret=LTI_CONSUMER_SECRET, + signature_type=SIGNATURE_TYPE_BODY, + ) + + def test_lti_login(self): + # The user initiates a login from an external site + (uri, _headers, body) = self.lti.sign( + uri=LTI_TPA_LOGIN_URL, http_method='POST', + headers={'Content-Type': FORM_ENCODED}, + body={ + 'user_id': LTI_USER_ID, + 'custom_tpa_next': '/account/finish_auth/?course_id=my_course_id&enrollment_action=enroll', + } + ) + login_response = self.client.post(path=uri, content_type=FORM_ENCODED, data=body) + # The user should be redirected to the registration form + self.assertEqual(login_response.status_code, 302) + self.assertTrue(login_response['Location'].endswith(reverse('signin_user'))) + register_response = self.client.get(login_response['Location']) + self.assertEqual(register_response.status_code, 200) + self.assertIn('currentProvider": "LTI Test Tool Consumer"', register_response.content) + self.assertIn('"errorMessage": null', register_response.content) + + # Now complete the form: + ajax_register_response = self.client.post( + reverse('user_api_registration'), + { + 'email': EMAIL, + 'name': 'Myself', + 'username': EDX_USER_ID, + 'honor_code': True, + } + ) + self.assertEqual(ajax_register_response.status_code, 200) + continue_response = self.client.get(LTI_TPA_COMPLETE_URL) + # The user should be redirected to the finish_auth view which will enroll them. + # FinishAuthView.js reads the URL parameters directly from $.url + self.assertEqual(continue_response.status_code, 302) + self.assertEqual( + continue_response['Location'], + 'http://testserver/account/finish_auth/?course_id=my_course_id&enrollment_action=enroll' + ) + + # Now check that we can login again + self.client.logout() + self.verify_user_email(EMAIL) + (uri, _headers, body) = self.lti.sign( + uri=LTI_TPA_LOGIN_URL, http_method='POST', + headers={'Content-Type': FORM_ENCODED}, + body={'user_id': LTI_USER_ID} + ) + login_2_response = self.client.post(path=uri, content_type=FORM_ENCODED, data=body) + # The user should be redirected to the dashboard + self.assertEqual(login_2_response.status_code, 302) + self.assertEqual(login_2_response['Location'], LTI_TPA_COMPLETE_URL) + continue_2_response = self.client.get(login_2_response['Location']) + self.assertEqual(continue_2_response.status_code, 302) + self.assertTrue(continue_2_response['Location'].endswith(reverse('dashboard'))) + + # Check that the user was created correctly + user = User.objects.get(email=EMAIL) + self.assertEqual(user.username, EDX_USER_ID) + + def test_reject_initiating_login(self): + response = self.client.get(LTI_TPA_LOGIN_URL) + self.assertEqual(response.status_code, 405) # Not Allowed + + def test_reject_bad_login(self): + login_response = self.client.post( + path=LTI_TPA_LOGIN_URL, content_type=FORM_ENCODED, + data="invalid=login" + ) + # The user should be redirected to the login page with an error message + # (auth_entry defaults to login for this provider) + self.assertEqual(login_response.status_code, 302) + self.assertTrue(login_response['Location'].endswith(reverse('signin_user'))) + error_response = self.client.get(login_response['Location']) + self.assertIn( + 'Authentication failed: LTI parameters could not be validated.', + error_response.content + ) + + def test_can_load_consumer_secret_from_settings(self): + lti = Client( + client_key=OTHER_LTI_CONSUMER_KEY, + client_secret=OTHER_LTI_CONSUMER_SECRET, + signature_type=SIGNATURE_TYPE_BODY, + ) + (uri, _headers, body) = lti.sign( + uri=LTI_TPA_LOGIN_URL, http_method='POST', + headers={'Content-Type': FORM_ENCODED}, + body={ + 'user_id': LTI_USER_ID, + 'custom_tpa_next': '/account/finish_auth/?course_id=my_course_id&enrollment_action=enroll', + } + ) + with self.settings(SOCIAL_AUTH_LTI_CONSUMER_SECRETS={OTHER_LTI_CONSUMER_KEY: OTHER_LTI_CONSUMER_SECRET}): + login_response = self.client.post(path=uri, content_type=FORM_ENCODED, data=body) + # The user should be redirected to the registration form + self.assertEqual(login_response.status_code, 302) + self.assertTrue(login_response['Location'].endswith(reverse('signin_user'))) + register_response = self.client.get(login_response['Location']) + self.assertEqual(register_response.status_code, 200) + self.assertIn( + 'currentProvider": "Tool Consumer with Secret in Settings"', + register_response.content + ) + self.assertIn('"errorMessage": null', register_response.content) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index aacb945aa6..dc4ca99880 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -1,7 +1,6 @@ """ Third_party_auth integration tests using a mock version of the TestShib provider """ -from django.contrib.auth.models import User from django.core.urlresolvers import reverse import httpretty from mock import patch @@ -38,7 +37,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): def metadata_callback(_request, _uri, headers): """ Return a cached copy of TestShib's metadata by reading it from disk """ - return (200, headers, self._read_data_file('testshib_metadata.xml')) + return (200, headers, self.read_data_file('testshib_metadata.xml')) httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type='text/xml', body=metadata_callback) self.addCleanup(httpretty.disable) self.addCleanup(httpretty.reset) @@ -106,7 +105,7 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): # Now check that we can login again: self.client.logout() - self._verify_user_email('myself@testshib.org') + self.verify_user_email('myself@testshib.org') self._test_return_login() def test_login(self): @@ -220,11 +219,5 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): return self.client.post( TPA_TESTSHIB_COMPLETE_URL, content_type='application/x-www-form-urlencoded', - data=self._read_data_file('testshib_response.txt'), + data=self.read_data_file('testshib_response.txt'), ) - - def _verify_user_email(self, email): - """ Mark the user with the given email as verified """ - user = User.objects.get(email=email) - user.is_active = True - user.save() diff --git a/common/djangoapps/third_party_auth/tests/test_lti.py b/common/djangoapps/third_party_auth/tests/test_lti.py new file mode 100644 index 0000000000..9e0f9122c3 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/test_lti.py @@ -0,0 +1,133 @@ +""" +Unit tests for third_party_auth LTI auth providers +""" + +import unittest +from oauthlib.common import Request +from third_party_auth.lti import LTIAuthBackend, LTI_PARAMS_KEY +from third_party_auth.tests.testutil import ThirdPartyAuthTestMixin + + +class UnitTestLTI(unittest.TestCase, ThirdPartyAuthTestMixin): + """ + Unit tests for third_party_auth LTI auth providers + """ + + def test_get_user_details_missing_keys(self): + lti = LTIAuthBackend() + details = lti.get_user_details({LTI_PARAMS_KEY: { + 'lis_person_name_full': 'Full name' + }}) + self.assertEquals(details, { + 'fullname': 'Full name' + }) + + def test_get_user_details_extra_keys(self): + lti = LTIAuthBackend() + details = lti.get_user_details({LTI_PARAMS_KEY: { + 'lis_person_name_full': 'Full name', + 'lis_person_name_given': 'Given', + 'lis_person_name_family': 'Family', + 'email': 'user@example.com', + 'other': 'something else' + }}) + self.assertEquals(details, { + 'fullname': 'Full name', + 'first_name': 'Given', + 'last_name': 'Family', + 'email': 'user@example.com' + }) + + def test_get_user_id(self): + lti = LTIAuthBackend() + user_id = lti.get_user_id(None, {LTI_PARAMS_KEY: { + 'oauth_consumer_key': 'consumer', + 'user_id': 'user' + }}) + self.assertEquals(user_id, 'consumer:user') + + def test_validate_lti_valid_request(self): + request = Request( + uri='https://example.com/lti', + http_method='POST', + body=self.read_data_file('lti_valid_request.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436823554, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertTrue(parameters) + self.assertDictContainsSubset({ + 'custom_extra': 'parameter', + 'user_id': '292832126' + }, parameters) + + def test_validate_lti_valid_request_with_get_params(self): + request = Request( + uri='https://example.com/lti?user_id=292832126<i_version=LTI-1p0', + http_method='POST', + body=self.read_data_file('lti_valid_request_with_get_params.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436823554, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertTrue(parameters) + self.assertDictContainsSubset({ + 'custom_extra': 'parameter', + 'user_id': '292832126' + }, parameters) + + def test_validate_lti_old_timestamp(self): + request = Request( + uri='https://example.com/lti', + http_method='POST', + body=self.read_data_file('lti_old_timestamp.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436900000, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertFalse(parameters) + + def test_validate_lti_invalid_signature(self): + request = Request( + uri='https://example.com/lti', + http_method='POST', + body=self.read_data_file('lti_invalid_signature.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436823554, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertFalse(parameters) + + def test_validate_lti_cannot_add_get_params(self): + request = Request( + uri='https://example.com/lti?custom_another=parameter', + http_method='POST', + body=self.read_data_file('lti_cannot_add_get_params.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436823554, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertFalse(parameters) + + def test_validate_lti_garbage(self): + request = Request( + uri='https://example.com/lti', + http_method='POST', + body=self.read_data_file('lti_garbage.txt') + ) + parameters = LTIAuthBackend._get_validated_lti_params_from_values( # pylint: disable=protected-access + request=request, current_time=1436823554, + lti_consumer_valid=True, lti_consumer_secret='secret', + lti_max_timestamp_age=10 + ) + self.assertFalse(parameters) diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py index 12022d2bf7..40591163db 100644 --- a/common/djangoapps/third_party_auth/tests/testutil.py +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -6,11 +6,18 @@ Used by Django and non-Django tests; must not have Django deps. from contextlib import contextmanager from django.conf import settings +from django.contrib.auth.models import User import django.test import mock import os.path -from third_party_auth.models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, cache as config_cache +from third_party_auth.models import ( + OAuth2ProviderConfig, + SAMLProviderConfig, + SAMLConfiguration, + LTIProviderConfig, + cache as config_cache, +) AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' @@ -52,6 +59,13 @@ class ThirdPartyAuthTestMixin(object): obj.save() return obj + @staticmethod + def configure_lti_provider(**kwargs): + """ Update the settings for a LTI Tool Consumer third party auth provider """ + obj = LTIProviderConfig(**kwargs) + obj.save() + return obj + @classmethod def configure_google_provider(cls, **kwargs): """ Update the settings for the Google third party auth provider/backend """ @@ -92,6 +106,19 @@ class ThirdPartyAuthTestMixin(object): kwargs.setdefault("secret", "test") return cls.configure_oauth_provider(**kwargs) + @classmethod + def verify_user_email(cls, email): + """ Mark the user with the given email as verified """ + user = User.objects.get(email=email) + user.is_active = True + user.save() + + @staticmethod + def read_data_file(filename): + """ Read the contents of a file in the data folder """ + with open(os.path.join(os.path.dirname(__file__), 'data', filename)) as f: + return f.read() + class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase): """Base class for auth test cases.""" @@ -111,18 +138,12 @@ class SAMLTestCase(TestCase): @classmethod def _get_public_key(cls, key_name='saml_key'): """ Get a public key for use in the test. """ - return cls._read_data_file('{}.pub'.format(key_name)) + return cls.read_data_file('{}.pub'.format(key_name)) @classmethod def _get_private_key(cls, key_name='saml_key'): """ Get a private key for use in the test. """ - return cls._read_data_file('{}.key'.format(key_name)) - - @staticmethod - def _read_data_file(filename): - """ Read the contents of a file in the data folder """ - with open(os.path.join(os.path.dirname(__file__), 'data', filename)) as f: - return f.read() + return cls.read_data_file('{}.key'.format(key_name)) def enable_saml(self, **kwargs): """ Enable SAML support (via SAMLConfiguration, not for any particular provider) """ diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py index 5d366b2da3..69c600932b 100644 --- a/common/djangoapps/third_party_auth/urls.py +++ b/common/djangoapps/third_party_auth/urls.py @@ -2,11 +2,12 @@ from django.conf.urls import include, patterns, url -from .views import inactive_user_view, saml_metadata_view +from .views import inactive_user_view, saml_metadata_view, lti_login_and_complete_view urlpatterns = patterns( '', url(r'^auth/inactive', inactive_user_view), url(r'^auth/saml/metadata.xml', saml_metadata_view), + url(r'^auth/login/(?Plti)/$', lti_login_and_complete_view), url(r'^auth/', include('social.apps.django_app.urls', namespace='social')), ) diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py index ef0233f33c..58fd17c784 100644 --- a/common/djangoapps/third_party_auth/views.py +++ b/common/djangoapps/third_party_auth/views.py @@ -3,11 +3,17 @@ Extra views required for SSO """ from django.conf import settings from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseServerError, Http404 +from django.http import HttpResponse, HttpResponseServerError, Http404, HttpResponseNotAllowed from django.shortcuts import redirect +from django.views.decorators.csrf import csrf_exempt +import social +from social.apps.django_app.views import complete from social.apps.django_app.utils import load_strategy, load_backend +from social.utils import setting_name from .models import SAMLConfiguration +URL_NAMESPACE = getattr(settings, setting_name('URL_NAMESPACE'), None) or 'social' + def inactive_user_view(request): """ @@ -36,3 +42,15 @@ def saml_metadata_view(request): if not errors: return HttpResponse(content=metadata, content_type='text/xml') return HttpResponseServerError(content=', '.join(errors)) + + +@csrf_exempt +@social.apps.django_app.utils.psa('{0}:complete'.format(URL_NAMESPACE)) +def lti_login_and_complete_view(request, backend, *args, **kwargs): + """This is a combination login/complete due to LTI being a one step login""" + + if request.method != 'POST': + return HttpResponseNotAllowed('POST') + + request.backend.start() + return complete(request, backend, *args, **kwargs) diff --git a/common/lib/xmodule/xmodule/js/js_test.yml b/common/lib/xmodule/xmodule/js/js_test.yml index 6c72d4b649..d7816e44bc 100644 --- a/common/lib/xmodule/xmodule/js/js_test.yml +++ b/common/lib/xmodule/xmodule/js/js_test.yml @@ -60,6 +60,7 @@ lib_paths: - public/js/split_test_staff.js - common_static/js/src/accessibility_tools.js - common_static/js/vendor/moment.min.js + - spec/main_requirejs.js # Paths to spec (test) JavaScript files spec_paths: diff --git a/common/lib/xmodule/xmodule/js/spec/main_requirejs.js b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js new file mode 100644 index 0000000000..405c92801d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/main_requirejs.js @@ -0,0 +1,11 @@ +(function(requirejs) { + requirejs.config({ + paths: { + "moment": "xmodule/include/common_static/js/vendor/moment.min" + }, + "moment": { + exports: "moment" + } + }); + +}).call(this, RequireJS.requirejs); diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index 59b0133d24..3808ec7c4c 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -14,10 +14,9 @@ define( 'video/01_initialize.js', -['video/03_video_player.js', 'video/00_i18n.js'], -function (VideoPlayer, i18n) { - var moment = window.moment; - +['video/03_video_player.js', 'video/00_i18n.js', 'moment'], +function (VideoPlayer, i18n, moment) { + var moment = moment || window.moment; /** * @function * diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index a49c7dbd9a..65bb345e1d 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -280,21 +280,6 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): courses[course_id] = course return courses.values() - @strip_key - def get_courses_keys(self, **kwargs): - ''' - Returns a list containing the top level XModuleDescriptors keys of the courses in this modulestore. - ''' - courses = {} - for store in self.modulestores: - # filter out ones which were fetched from earlier stores but locations may not be == - for course in store.get_courses(**kwargs): - course_id = self._clean_locator_for_mapping(course.id) - if course_id not in courses: - # course is indeed unique. save it in result - courses[course_id] = course - return courses.keys() - @strip_key def get_libraries(self, **kwargs): """ diff --git a/common/static/common/js/components/collections/paging_collection.js b/common/static/common/js/components/collections/paging_collection.js index 4ac4bf93b3..4bf4ff3066 100644 --- a/common/static/common/js/components/collections/paging_collection.js +++ b/common/static/common/js/components/collections/paging_collection.js @@ -30,6 +30,8 @@ isZeroIndexed: false, perPage: 10, + isStale: false, + sortField: '', sortDirection: 'descending', sortableFields: {}, @@ -37,6 +39,8 @@ filterField: '', filterableFields: {}, + searchString: null, + paginator_core: { type: 'GET', dataType: 'json', @@ -51,9 +55,10 @@ }, server_api: { - 'page': function () { return this.currentPage; }, - 'page_size': function () { return this.perPage; }, - 'sort_order': function () { return this.sortField; } + page: function () { return this.currentPage; }, + page_size: function () { return this.perPage; }, + text_search: function () { return this.searchString ? this.searchString : ''; }, + sort_order: function () { return this.sortField; } }, parse: function (response) { @@ -61,7 +66,11 @@ this.currentPage = response.current_page; this.totalPages = response.num_pages; this.start = response.start; - this.sortField = response.sort_order; + + // Note: sort_order is not returned when performing a search + if (response.sort_order) { + this.sortField = response.sort_order; + } return response.results; }, @@ -84,6 +93,7 @@ self = this; return this.goTo(page - (this.isZeroIndexed ? 1 : 0), {reset: true}).then( function () { + self.isStale = false; self.trigger('page_changed'); }, function () { @@ -92,6 +102,24 @@ ); }, + + /** + * Refreshes the collection if it has been marked as stale. + * @returns {promise} Returns a promise representing the refresh. + */ + refresh: function() { + var deferred = $.Deferred(); + if (this.isStale) { + this.setPage(1) + .done(function() { + deferred.resolve(); + }); + } else { + deferred.resolve(); + } + return deferred.promise(); + }, + /** * Returns true if the collection has a next page, false otherwise. */ @@ -183,7 +211,7 @@ } } this.sortField = fieldName; - this.setPage(1); + this.isStale = true; }, /** @@ -193,7 +221,7 @@ */ setSortDirection: function (direction) { this.sortDirection = direction; - this.setPage(1); + this.isStale = true; }, /** @@ -203,7 +231,19 @@ */ setFilterField: function (fieldName) { this.filterField = fieldName; - this.setPage(1); + this.isStale = true; + }, + + /** + * Sets the string to use for a text search. If no string is specified then + * the search is cleared. + * @param searchString A string to search on, or null if no search is to be applied. + */ + setSearchString: function(searchString) { + if (searchString !== this.searchString) { + this.searchString = searchString; + this.isStale = true; + } } }, { SortDirection: { diff --git a/common/static/common/js/components/views/paging_header.js b/common/static/common/js/components/views/paging_header.js index 8cd01bb9a1..c49af8bc4b 100644 --- a/common/static/common/js/components/views/paging_header.js +++ b/common/static/common/js/components/views/paging_header.js @@ -43,10 +43,16 @@ return this; }, + /** + * Updates the collection's sort order, and fetches an updated set of + * results. + * @returns {*} A promise for the collection being updated + */ sortCollection: function () { var selected = this.$('#paging-header-select option:selected'); this.sortOrder = selected.attr('value'); this.collection.setSortField(this.sortOrder); + return this.collection.refresh(); } }); return PagingHeader; diff --git a/common/static/common/js/components/views/search_field.js b/common/static/common/js/components/views/search_field.js new file mode 100644 index 0000000000..7599edee35 --- /dev/null +++ b/common/static/common/js/components/views/search_field.js @@ -0,0 +1,71 @@ +/** + * A search field that works in concert with a paginated collection. When the user + * performs a search, the collection's search string will be updated and then the + * collection will be refreshed to show the first page of results. + */ +;(function (define) { + 'use strict'; + + define(['backbone', 'jquery', 'underscore', 'text!common/templates/components/search-field.underscore'], + function (Backbone, $, _, searchFieldTemplate) { + return Backbone.View.extend({ + + events: { + 'submit .search-form': 'performSearch', + 'blur .search-form': 'onFocusOut', + 'keyup .search-field': 'refreshState', + 'click .action-clear': 'clearSearch' + }, + + initialize: function(options) { + this.type = options.type; + this.label = options.label; + }, + + refreshState: function() { + var searchField = this.$('.search-field'), + clearButton = this.$('.action-clear'), + searchString = $.trim(searchField.val()); + if (searchString) { + clearButton.removeClass('is-hidden'); + } else { + clearButton.addClass('is-hidden'); + } + }, + + render: function() { + this.$el.html(_.template(searchFieldTemplate, { + type: this.type, + searchString: this.collection.searchString, + searchLabel: this.label + })); + this.refreshState(); + return this; + }, + + onFocusOut: function(event) { + // If the focus is going anywhere but the clear search + // button then treat it as a request to search. + if (!$(event.relatedTarget).hasClass('action-clear')) { + this.performSearch(event); + } + }, + + performSearch: function(event) { + var searchField = this.$('.search-field'), + searchString = $.trim(searchField.val()); + event.preventDefault(); + this.collection.setSearchString(searchString); + return this.collection.refresh(); + }, + + clearSearch: function(event) { + event.preventDefault(); + this.$('.search-field').val(''); + this.collection.setSearchString(''); + this.refreshState(); + return this.collection.refresh(); + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/common/static/common/js/spec/components/paging_collection_spec.js b/common/static/common/js/spec/components/paging_collection_spec.js index 89062d24d2..0d5f668e97 100644 --- a/common/static/common/js/spec/components/paging_collection_spec.js +++ b/common/static/common/js/spec/components/paging_collection_spec.js @@ -10,11 +10,11 @@ define(['jquery', 'use strict'; describe('PagingCollection', function () { - var collection, requests, server, assertQueryParams; - server = { + var collection; + var server = { isZeroIndexed: false, count: 43, - respond: function () { + respond: function (requests) { var params = (new URI(requests[requests.length - 1].url)).query(true), page = parseInt(params['page'], 10), page_size = parseInt(params['page_size'], 10), @@ -35,7 +35,7 @@ define(['jquery', } } }; - assertQueryParams = function (params) { + var assertQueryParams = function (requests, params) { var urlParams = (new URI(requests[requests.length - 1].url)).query(true); _.each(params, function (value, key) { expect(urlParams[key]).toBe(value); @@ -45,7 +45,6 @@ define(['jquery', beforeEach(function () { collection = new PagingCollection(); collection.perPage = 10; - requests = AjaxHelpers.requests(this); server.isZeroIndexed = false; server.count = 43; }); @@ -69,10 +68,11 @@ define(['jquery', }); it('can set the sort field', function () { + var requests = AjaxHelpers.requests(this); collection.registerSortableField('test_field', 'Test Field'); collection.setSortField('test_field', false); - expect(requests.length).toBe(1); - assertQueryParams({'sort_order': 'test_field'}); + collection.refresh(); + assertQueryParams(requests, {'sort_order': 'test_field'}); expect(collection.sortField).toBe('test_field'); expect(collection.sortDisplayName()).toBe('Test Field'); }); @@ -80,7 +80,7 @@ define(['jquery', it('can set the filter field', function () { collection.registerFilterableField('test_field', 'Test Field'); collection.setFilterField('test_field'); - expect(requests.length).toBe(1); + collection.refresh(); // The default implementation does not send any query params for filtering expect(collection.filterField).toBe('test_field'); expect(collection.filterDisplayName()).toBe('Test Field'); @@ -88,11 +88,9 @@ define(['jquery', it('can set the sort direction', function () { collection.setSortDirection(PagingCollection.SortDirection.ASCENDING); - expect(requests.length).toBe(1); // The default implementation does not send any query params for sort direction expect(collection.sortDirection).toBe(PagingCollection.SortDirection.ASCENDING); collection.setSortDirection(PagingCollection.SortDirection.DESCENDING); - expect(requests.length).toBe(2); expect(collection.sortDirection).toBe(PagingCollection.SortDirection.DESCENDING); }); @@ -113,11 +111,12 @@ define(['jquery', 'queries with page, page_size, and sort_order parameters when zero indexed': [true, 2], 'queries with page, page_size, and sort_order parameters when one indexed': [false, 3], }, function (isZeroIndexed, page) { + var requests = AjaxHelpers.requests(this); collection.isZeroIndexed = isZeroIndexed; collection.perPage = 5; collection.sortField = 'test_field'; collection.setPage(3); - assertQueryParams({'page': page.toString(), 'page_size': '5', 'sort_order': 'test_field'}); + assertQueryParams(requests, {'page': page.toString(), 'page_size': '5', 'sort_order': 'test_field'}); }); SpecHelpers.withConfiguration({ @@ -129,27 +128,30 @@ define(['jquery', }, function () { describe('setPage', function() { it('triggers a reset event when the page changes successfully', function () { - var resetTriggered = false; + var requests = AjaxHelpers.requests(this), + resetTriggered = false; collection.on('reset', function () { resetTriggered = true; }); collection.setPage(3); - server.respond(); + server.respond(requests); expect(resetTriggered).toBe(true); }); it('triggers an error event when the requested page is out of range', function () { - var errorTriggered = false; + var requests = AjaxHelpers.requests(this), + errorTriggered = false; collection.on('error', function () { errorTriggered = true; }); collection.setPage(17); - server.respond(); + server.respond(requests); expect(errorTriggered).toBe(true); }); it('triggers an error event if the server responds with a 500', function () { - var errorTriggered = false; + var requests = AjaxHelpers.requests(this), + errorTriggered = false; collection.on('error', function () { errorTriggered = true; }); collection.setPage(2); expect(collection.getPage()).toBe(2); - server.respond(); + server.respond(requests); collection.setPage(3); AjaxHelpers.respondWithError(requests, 500, {}, requests.length - 1); expect(errorTriggered).toBe(true); @@ -159,11 +161,12 @@ define(['jquery', describe('getPage', function () { it('returns the correct page', function () { + var requests = AjaxHelpers.requests(this); collection.setPage(1); - server.respond(); + server.respond(requests); expect(collection.getPage()).toBe(1); collection.setPage(3); - server.respond(); + server.respond(requests); expect(collection.getPage()).toBe(3); }); }); @@ -177,9 +180,10 @@ define(['jquery', 'returns false on the last page': [5, 43, false] }, function (page, count, result) { + var requests = AjaxHelpers.requests(this); server.count = count; collection.setPage(page); - server.respond(); + server.respond(requests); expect(collection.hasNextPage()).toBe(result); } ); @@ -194,9 +198,10 @@ define(['jquery', 'returns false on the first page': [1, 43, false] }, function (page, count, result) { + var requests = AjaxHelpers.requests(this); server.count = count; collection.setPage(page); - server.respond(); + server.respond(requests); expect(collection.hasPreviousPage()).toBe(result); } ); @@ -209,13 +214,14 @@ define(['jquery', 'silently fails on the last page': [5, 43, 5] }, function (page, count, newPage) { + var requests = AjaxHelpers.requests(this); server.count = count; collection.setPage(page); - server.respond(); + server.respond(requests); expect(collection.getPage()).toBe(page); collection.nextPage(); if (requests.length > 1) { - server.respond(); + server.respond(requests); } expect(collection.getPage()).toBe(newPage); } @@ -229,13 +235,14 @@ define(['jquery', 'silently fails on the first page': [1, 43, 1] }, function (page, count, newPage) { + var requests = AjaxHelpers.requests(this); server.count = count; collection.setPage(page); - server.respond(); + server.respond(requests); expect(collection.getPage()).toBe(page); collection.previousPage(); if (requests.length > 1) { - server.respond(); + server.respond(requests); } expect(collection.getPage()).toBe(newPage); } diff --git a/common/static/common/js/spec/components/search_field_spec.js b/common/static/common/js/spec/components/search_field_spec.js new file mode 100644 index 0000000000..3464ef1036 --- /dev/null +++ b/common/static/common/js/spec/components/search_field_spec.js @@ -0,0 +1,105 @@ +define([ + 'underscore', + 'common/js/components/views/search_field', + 'common/js/components/collections/paging_collection', + 'common/js/spec_helpers/ajax_helpers' +], function (_, SearchFieldView, PagingCollection, AjaxHelpers) { + 'use strict'; + describe('SearchFieldView', function () { + var searchFieldView, + mockUrl = '/api/mock_collection'; + + var newCollection = function (size, perPage) { + var pageSize = 5, + results = _.map(_.range(size), function (i) { return {foo: i}; }); + var collection = new PagingCollection( + [], + { + url: mockUrl, + count: results.length, + num_pages: results.length / pageSize, + current_page: 1, + start: 0, + results: _.first(results, perPage) + }, + {parse: true} + ); + collection.start = 0; + collection.totalCount = results.length; + return collection; + }; + + var createSearchFieldView = function (options) { + options = _.extend( + { + type: 'test', + collection: newCollection(5, 4), + el: $('.test-search') + }, + options || {} + ); + return new SearchFieldView(options); + }; + + beforeEach(function() { + setFixtures(''); + }); + + it('correctly displays itself', function () { + searchFieldView = createSearchFieldView().render(); + expect(searchFieldView.$('.search-field').val(), ''); + expect(searchFieldView.$('.action-clear')).toHaveClass('is-hidden'); + }); + + it('can display with an initial search string', function () { + searchFieldView = createSearchFieldView({ + searchString: 'foo' + }).render(); + expect(searchFieldView.$('.search-field').val(), 'foo'); + }); + + it('refreshes the collection when performing a search', function () { + var requests = AjaxHelpers.requests(this); + searchFieldView = createSearchFieldView().render(); + searchFieldView.$('.search-field').val('foo'); + searchFieldView.$('.action-search').click(); + AjaxHelpers.expectRequestURL(requests, mockUrl, { + page: '1', + page_size: '10', + sort_order: '', + text_search: 'foo' + }); + AjaxHelpers.respondWithJson(requests, { + count: 10, + current_page: 1, + num_pages: 1, + start: 0, + results: [] + }); + expect(searchFieldView.$('.search-field').val(), 'foo'); + }); + + it('can clear the search', function () { + var requests = AjaxHelpers.requests(this); + searchFieldView = createSearchFieldView({ + searchString: 'foo' + }).render(); + searchFieldView.$('.action-clear').click(); + AjaxHelpers.expectRequestURL(requests, mockUrl, { + page: '1', + page_size: '10', + sort_order: '', + text_search: '' + }); + AjaxHelpers.respondWithJson(requests, { + count: 10, + current_page: 1, + num_pages: 1, + start: 0, + results: [] + }); + expect(searchFieldView.$('.search-field').val(), ''); + expect(searchFieldView.$('.action-clear')).toHaveClass('is-hidden'); + }); + }); +}); diff --git a/common/static/common/js/spec_helpers/ajax_helpers.js b/common/static/common/js/spec_helpers/ajax_helpers.js index 7f0ce09ecc..e699805512 100644 --- a/common/static/common/js/spec_helpers/ajax_helpers.js +++ b/common/static/common/js/spec_helpers/ajax_helpers.js @@ -1,7 +1,7 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { 'use strict'; - var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectJsonRequestURL, + var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectRequestURL, respondWithJson, respondWithError, respondWithTextError, respondWithNoContent; /* These utility methods are used by Jasmine tests to create a mock server or @@ -77,7 +77,7 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { * @param expectedParameters An object representing the URL parameters * @param requestIndex An optional index for the request (by default, the last request is used) */ - expectJsonRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) { + expectRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) { var request, parameters; if (_.isUndefined(requestIndex)) { requestIndex = requests.length - 1; @@ -153,15 +153,15 @@ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { }; return { - 'server': fakeServer, - 'requests': fakeRequests, - 'expectRequest': expectRequest, - 'expectJsonRequest': expectJsonRequest, - 'expectJsonRequestURL': expectJsonRequestURL, - 'expectPostRequest': expectPostRequest, - 'respondWithJson': respondWithJson, - 'respondWithError': respondWithError, - 'respondWithTextError': respondWithTextError, - 'respondWithNoContent': respondWithNoContent, + server: fakeServer, + requests: fakeRequests, + expectRequest: expectRequest, + expectJsonRequest: expectJsonRequest, + expectPostRequest: expectPostRequest, + expectRequestURL: expectRequestURL, + respondWithJson: respondWithJson, + respondWithError: respondWithError, + respondWithTextError: respondWithTextError, + respondWithNoContent: respondWithNoContent }; }); diff --git a/common/static/common/templates/components/search-field.underscore b/common/static/common/templates/components/search-field.underscore new file mode 100644 index 0000000000..aac29f640d --- /dev/null +++ b/common/static/common/templates/components/search-field.underscore @@ -0,0 +1,12 @@ + diff --git a/common/static/common/templates/components/system-feedback.underscore b/common/static/common/templates/components/system-feedback.underscore index 09b1153289..e33e3149eb 100644 --- a/common/static/common/templates/components/system-feedback.underscore +++ b/common/static/common/templates/components/system-feedback.underscore @@ -15,8 +15,8 @@ <% } %>
-

<%= title %>

- <% if(obj.message) { %>

<%= message %>

<% } %> +

<%- title %>

+ <% if(obj.message) { %>

<%- message %>

<% } %>
<% if(obj.actions) { %> @@ -24,13 +24,13 @@
    <% if(actions.primary) { %> <% } %> <% if(actions.secondary) { _.each(actions.secondary, function(secondary) { %> <% }); } %> diff --git a/common/static/js/capa/drag_and_drop/draggables.js b/common/static/js/capa/drag_and_drop/draggables.js index a45bc519a3..faea44b761 100644 --- a/common/static/js/capa/drag_and_drop/draggables.js +++ b/common/static/js/capa/drag_and_drop/draggables.js @@ -289,7 +289,7 @@ define(['js/capa/drag_and_drop/draggable_events', 'js/capa/drag_and_drop/draggab draggableObj.iconEl.appendTo(draggableObj.containerEl); - draggableObj.iconWidth = draggableObj.iconEl.width(); + draggableObj.iconWidth = draggableObj.iconEl.width() + 1; draggableObj.iconHeight = draggableObj.iconEl.height(); draggableObj.iconWidthSmall = draggableObj.iconWidth; draggableObj.iconHeightSmall = draggableObj.iconHeight; diff --git a/common/static/js/spec/main_requirejs.js b/common/static/js/spec/main_requirejs.js index ac29c68b3c..24b00f2185 100644 --- a/common/static/js/spec/main_requirejs.js +++ b/common/static/js/spec/main_requirejs.js @@ -155,13 +155,14 @@ define([ // Run the common tests that use RequireJS. + 'common-requirejs/include/common/js/spec/components/feedback_spec.js', 'common-requirejs/include/common/js/spec/components/list_spec.js', 'common-requirejs/include/common/js/spec/components/paginated_view_spec.js', 'common-requirejs/include/common/js/spec/components/paging_collection_spec.js', 'common-requirejs/include/common/js/spec/components/paging_header_spec.js', 'common-requirejs/include/common/js/spec/components/paging_footer_spec.js', - 'common-requirejs/include/common/js/spec/components/view_utils_spec.js', - 'common-requirejs/include/common/js/spec/components/feedback_spec.js' + 'common-requirejs/include/common/js/spec/components/search_field_spec.js', + 'common-requirejs/include/common/js/spec/components/view_utils_spec.js' ]); }).call(this, requirejs, define); diff --git a/common/test/acceptance/pages/lms/auto_auth.py b/common/test/acceptance/pages/lms/auto_auth.py index d9113719e1..1e4d0681ba 100644 --- a/common/test/acceptance/pages/lms/auto_auth.py +++ b/common/test/acceptance/pages/lms/auto_auth.py @@ -17,7 +17,8 @@ class AutoAuthPage(PageObject): CONTENT_REGEX = r'.+? user (?P\S+) \((?P.+?)\) with password \S+ and user_id (?P\d+)$' - def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None): + def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, + enrollment_mode=None, roles=None): """ Auto-auth is an end-point for HTTP GET requests. By default, it will create accounts with random user credentials, @@ -52,6 +53,8 @@ class AutoAuthPage(PageObject): if course_id is not None: self._params['course_id'] = course_id + if enrollment_mode: + self._params['enrollment_mode'] = enrollment_mode if roles is not None: self._params['roles'] = roles diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py index bc9efe67ec..2794b3584a 100644 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ b/common/test/acceptance/pages/lms/login_and_register.py @@ -127,7 +127,7 @@ class CombinedLoginAndRegisterPage(PageObject): @property def url(self): """Return the URL for the combined login/registration page. """ - url = "{base}/account/{login_or_register}".format( + url = "{base}/{login_or_register}".format( base=BASE_URL, login_or_register=self._start_page ) diff --git a/common/test/acceptance/pages/lms/teams.py b/common/test/acceptance/pages/lms/teams.py index 1f3779efaf..9ad67de7c8 100644 --- a/common/test/acceptance/pages/lms/teams.py +++ b/common/test/acceptance/pages/lms/teams.py @@ -20,6 +20,25 @@ TEAMS_HEADER_CSS = '.teams-header' CREATE_TEAM_LINK_CSS = '.create-team' +class TeamCardsMixin(object): + """Provides common operations on the team card component.""" + + @property + def team_cards(self): + """Get all the team cards on the page.""" + return self.q(css='.team-card') + + @property + def team_names(self): + """Return the names of each team on the page.""" + return self.q(css='h3.card-title').map(lambda e: e.text).results + + @property + def team_descriptions(self): + """Return the names of each team on the page.""" + return self.q(css='p.card-description').map(lambda e: e.text).results + + class TeamsPage(CoursePage): """ Teams page/tab. @@ -84,7 +103,7 @@ class TeamsPage(CoursePage): self.q(css='a.nav-item').filter(text=topic)[0].click() -class MyTeamsPage(CoursePage, PaginatedUIMixin): +class MyTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin): """ The 'My Teams' tab of the Teams page. """ @@ -98,11 +117,6 @@ class MyTeamsPage(CoursePage, PaginatedUIMixin): return False return 'is-active' in button_classes[0] - @property - def team_cards(self): - """Get all the team cards on the page.""" - return self.q(css='.team-card') - class BrowseTopicsPage(CoursePage, PaginatedUIMixin): """ @@ -128,6 +142,11 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): """Return a list of the topic names present on the page.""" return self.q(css=CARD_TITLE_CSS).map(lambda e: e.text).results + @property + def topic_descriptions(self): + """Return a list of the topic descriptions present on the page.""" + return self.q(css='p.card-description').map(lambda e: e.text).results + def browse_teams_for_topic(self, topic_name): """ Show the teams list for `topic_name`. @@ -145,43 +164,43 @@ class BrowseTopicsPage(CoursePage, PaginatedUIMixin): self.wait_for_ajax() -class BrowseTeamsPage(CoursePage, PaginatedUIMixin): +class BaseTeamsPage(CoursePage, PaginatedUIMixin, TeamCardsMixin): """ The paginated UI for browsing teams within a Topic on the Teams page. """ def __init__(self, browser, course_id, topic): """ - Set up `self.url_path` on instantiation, since it dynamically - reflects the current topic. Note that `topic` is a dict - representation of a topic following the same convention as a - course module's topic. + Note that `topic` is a dict representation of a topic following + the same convention as a course module's topic. """ - super(BrowseTeamsPage, self).__init__(browser, course_id) + super(BaseTeamsPage, self).__init__(browser, course_id) self.topic = topic - self.url_path = "teams/#topics/{topic_id}".format(topic_id=self.topic['id']) def is_browser_on_page(self): - """Check if we're on the teams list page for a particular topic.""" - self.wait_for_element_presence('.team-actions', 'Wait for the bottom links to be present') + """Check if we're on a teams list page for a particular topic.""" has_correct_url = self.url.endswith(self.url_path) teams_list_view_present = self.q(css='.teams-main').present return has_correct_url and teams_list_view_present @property - def header_topic_name(self): + def header_name(self): """Get the topic name displayed by the page header""" return self.q(css=TEAMS_HEADER_CSS + ' .page-title')[0].text @property - def header_topic_description(self): + def header_description(self): """Get the topic description displayed by the page header""" return self.q(css=TEAMS_HEADER_CSS + ' .page-description')[0].text @property - def team_cards(self): - """Get all the team cards on the page.""" - return self.q(css='.team-card') + def sort_order(self): + """Return the current sort order on the page.""" + return self.q( + css='#paging-header-select option' + ).filter( + lambda e: e.is_selected() + ).results[0].text.strip() def click_create_team_link(self): """ Click on create team link.""" @@ -204,6 +223,55 @@ class BrowseTeamsPage(CoursePage, PaginatedUIMixin): query.first.click() self.wait_for_ajax() + def sort_teams_by(self, sort_order): + """Sort the list of teams by the given `sort_order`.""" + self.q( + css='#paging-header-select option[value={sort_order}]'.format(sort_order=sort_order) + ).click() + self.wait_for_ajax() + + @property + def _showing_search_results(self): + """ + Returns true if showing search results. + """ + return self.header_description.startswith(u"Showing results for") + + def search(self, string): + """ + Searches for the specified string, and returns a SearchTeamsPage + representing the search results page. + """ + self.q(css='.search-field').first.fill(string) + self.q(css='.action-search').first.click() + self.wait_for( + lambda: self._showing_search_results, + description="Showing search results" + ) + page = SearchTeamsPage(self.browser, self.course_id, self.topic) + page.wait_for_page() + return page + + +class BrowseTeamsPage(BaseTeamsPage): + """ + The paginated UI for browsing teams within a Topic on the Teams + page. + """ + def __init__(self, browser, course_id, topic): + super(BrowseTeamsPage, self).__init__(browser, course_id, topic) + self.url_path = "teams/#topics/{topic_id}".format(topic_id=self.topic['id']) + + +class SearchTeamsPage(BaseTeamsPage): + """ + The paginated UI for showing team search results. + page. + """ + def __init__(self, browser, course_id, topic): + super(SearchTeamsPage, self).__init__(browser, course_id, topic) + self.url_path = "teams/#topics/{topic_id}/search".format(topic_id=self.topic['id']) + class CreateOrEditTeamPage(CoursePage, FieldsMixin): """ diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 2aa5ac2310..9966eeff4f 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -305,7 +305,7 @@ class ContainerPage(PageObject): Returns: list """ - css = '#tab{tab_index} a[data-category={category_type}] span'.format( + css = '#tab{tab_index} button[data-category={category_type}] span'.format( tab_index=tab_index, category_type=category_type ) diff --git a/common/test/acceptance/pages/studio/settings_certificates.py b/common/test/acceptance/pages/studio/settings_certificates.py index 5916de4359..42fdfe84ad 100644 --- a/common/test/acceptance/pages/studio/settings_certificates.py +++ b/common/test/acceptance/pages/studio/settings_certificates.py @@ -27,6 +27,12 @@ class CertificatesPage(CoursePage): # Helpers ################ + def refresh(self): + """ + Refresh the certificate page + """ + self.browser.refresh() + def is_browser_on_page(self): """ Verify that the browser is on the page and it is not still loading. @@ -434,11 +440,8 @@ class Signatory(object): """ Save signatory. """ - # Move focus from input to save button and then click it - self.certificate.page.browser.execute_script( - "$('{} .signatory-panel-save').focus()".format(self.get_selector()) - ) - self.find_css('.signatory-panel-save').first.click() + # Click on the save button. + self.certificate.page.q(css='button.signatory-panel-save').click() self.mode = 'details' self.certificate.page.wait_for_ajax() self.wait_for_signatory_detail_view() @@ -447,7 +450,7 @@ class Signatory(object): """ Cancel signatory editing. """ - self.find_css('.signatory-panel-close').first.click() + self.certificate.page.q(css='button.signatory-panel-close').click() self.mode = 'details' self.wait_for_signatory_detail_view() diff --git a/common/test/acceptance/pages/studio/users.py b/common/test/acceptance/pages/studio/users.py index e8da392f46..046ce14147 100644 --- a/common/test/acceptance/pages/studio/users.py +++ b/common/test/acceptance/pages/studio/users.py @@ -33,9 +33,9 @@ class UsersPageMixin(PageObject): def is_browser_on_page(self): """ - Returns True iff the browser has loaded the page. + Returns True if the browser has loaded the page. """ - return self.q(css='body.view-team').present + return self.q(css='body.view-team').present and not self.q(css='.ui-loading').present @property def users(self): diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index d2394db5a9..dbcea8c6b5 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -72,7 +72,7 @@ def add_discussion(page, menu_index=0): placement within the page). """ page.wait_for_component_menu() - click_css(page, 'a>span.large-discussion-icon', menu_index) + click_css(page, 'button>span.large-discussion-icon', menu_index) def add_advanced_component(page, menu_index, name): @@ -84,7 +84,7 @@ def add_advanced_component(page, menu_index, name): """ # Click on the Advanced icon. page.wait_for_component_menu() - click_css(page, 'a>span.large-advanced-icon', menu_index, require_notification=False) + click_css(page, 'button>span.large-advanced-icon', menu_index, require_notification=False) # This does an animation to hide the first level of buttons # and instead show the Advanced buttons that are available. @@ -95,7 +95,7 @@ def add_advanced_component(page, menu_index, name): page.wait_for_element_visibility('.new-component-advanced', 'Advanced component menu is visible') # Now click on the component to add it. - component_css = 'a[data-category={}]'.format(name) + component_css = 'button[data-category={}]'.format(name) page.wait_for_element_visibility(component_css, 'Advanced component {} is visible'.format(name)) # Adding some components, e.g. the Discussion component, will make an ajax call @@ -123,7 +123,7 @@ def add_component(page, item_type, specific_type): 'Wait for the add component menu to disappear' ) - all_options = page.q(css='.new-component-{} ul.new-component-template li a span'.format(item_type)) + all_options = page.q(css='.new-component-{} ul.new-component-template li button span'.format(item_type)) chosen_option = all_options.filter(lambda el: el.text == specific_type).first chosen_option.click() wait_for_notification(page) @@ -139,13 +139,13 @@ def add_html_component(page, menu_index, boilerplate=None): """ # Click on the HTML icon. page.wait_for_component_menu() - click_css(page, 'a>span.large-html-icon', menu_index, require_notification=False) + click_css(page, 'button>span.large-html-icon', menu_index, require_notification=False) # Make sure that the menu of HTML components is visible before clicking page.wait_for_element_visibility('.new-component-html', 'HTML component menu is visible') # Now click on the component to add it. - component_css = 'a[data-category=html]' + component_css = 'button[data-category=html]' if boilerplate: component_css += '[data-boilerplate={}]'.format(boilerplate) else: diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py index 39cf5b996f..b35b620f92 100644 --- a/common/test/acceptance/pages/studio/video/video.py +++ b/common/test/acceptance/pages/studio/video/video.py @@ -30,7 +30,7 @@ CLASS_SELECTORS = { } BUTTON_SELECTORS = { - 'create_video': 'a[data-category="video"]', + 'create_video': 'button[data-category="video"]', 'handout_download': '.video-handout.video-download-button a', 'handout_download_editor': '.wrapper-comp-setting.file-uploader .download-action', 'upload_asset': '.upload-action', diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index ca0a6a260e..cd9cedd386 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -109,7 +109,7 @@ class LoginFromCombinedPageTest(UniqueCourseTest): self.login_page.visit().toggle_form() self.assertEqual(self.login_page.current_form, "register") - @flaky # TODO fix this, see ECOM-1165 + @flaky # ECOM-1165 def test_password_reset_success(self): # Create a user account email, password = self._create_unique_user() # pylint: disable=unused-variable diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py index 5769934b94..6915554052 100644 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -168,32 +168,12 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): # Auto-auth register for the course. self._auto_auth(self.USERNAME, self.EMAIL, False) - def _auto_auth(self, username, email, staff): + def _auto_auth(self, username, email, staff, enrollment_mode="honor"): """ Logout and login with given credentials. """ AutoAuthPage(self.browser, username=username, email=email, - course_id=self.course_id, staff=staff).visit() - - def _login_as_a_verified_user(self): - """ - login as a verififed user - """ - - self._auto_auth(self.USERNAME, self.EMAIL, False) - - # the track selection page cannot be visited. see the other tests to see if any prereq is there. - # Navigate to the track selection page - self.track_selection_page.visit() - - # Enter the payment and verification flow by choosing to enroll as verified - self.track_selection_page.enroll('verified') - - # Proceed to the fake payment page - self.payment_and_verification_flow.proceed_to_payment() - - # Submit payment - self.fake_payment_page.submit_payment() + course_id=self.course_id, staff=staff, enrollment_mode=enrollment_mode).visit() def _create_a_proctored_exam_and_attempt(self): """ @@ -212,7 +192,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): # login as a verified student and visit the courseware. LogoutPage(self.browser).visit() - self._login_as_a_verified_user() + self._auto_auth(self.USERNAME, self.EMAIL, False, enrollment_mode="verified") self.courseware_page.visit() # Start the proctored exam. @@ -235,7 +215,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): # login as a verified student and visit the courseware. LogoutPage(self.browser).visit() - self._login_as_a_verified_user() + self._auto_auth(self.USERNAME, self.EMAIL, False, enrollment_mode="verified") self.courseware_page.visit() # Start the proctored exam. diff --git a/common/test/acceptance/tests/lms/test_lms_problems.py b/common/test/acceptance/tests/lms/test_lms_problems.py index ddfa3c7d3b..6c9a3e7fe8 100644 --- a/common/test/acceptance/tests/lms/test_lms_problems.py +++ b/common/test/acceptance/tests/lms/test_lms_problems.py @@ -5,6 +5,7 @@ Bok choy acceptance tests for problems in the LMS See also old lettuce tests in lms/djangoapps/courseware/features/problems.feature """ from textwrap import dedent +from flaky import flaky from ..helpers import UniqueCourseTest from ...pages.studio.auto_auth import AutoAuthPage @@ -191,6 +192,7 @@ class ProblemHintWithHtmlTest(ProblemsTest, EventsTestMixin): """) return XBlockFixtureDesc('problem', 'PROBLEM HTML HINT TEST', data=xml) + @flaky # TODO fix this, see TNL-3183 def test_check_hint(self): """ Test clicking Check shows the extended hint in the problem message. diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index 84f327fb16..fcecf8eb3f 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -3,13 +3,16 @@ Acceptance tests for the teams feature. """ import json import random +import time +from dateutil.parser import parse import ddt from flaky import flaky from nose.plugins.attrib import attr from uuid import uuid4 +from unittest import skip -from ..helpers import UniqueCourseTest +from ..helpers import UniqueCourseTest, EventsTestMixin from ...fixtures import LMS_BASE_URL from ...fixtures.course import CourseFixture from ...fixtures.discussion import ( @@ -26,7 +29,7 @@ from ...pages.lms.teams import TeamsPage, MyTeamsPage, BrowseTopicsPage, BrowseT TOPICS_PER_PAGE = 12 -class TeamsTabBase(UniqueCourseTest): +class TeamsTabBase(EventsTestMixin, UniqueCourseTest): """Base class for Teams Tab tests""" def setUp(self): super(TeamsTabBase, self).setUp() @@ -38,7 +41,7 @@ class TeamsTabBase(UniqueCourseTest): """Create `num_topics` test topics.""" return [{u"description": i, u"name": i, u"id": i} for i in map(str, xrange(num_topics))] - def create_teams(self, topic, num_teams): + def create_teams(self, topic, num_teams, time_between_creation=0): """Create `num_teams` teams belonging to `topic`.""" teams = [] for i in xrange(num_teams): @@ -55,6 +58,10 @@ class TeamsTabBase(UniqueCourseTest): data=json.dumps(team), headers=self.course_fixture.headers ) + # Sadly, this sleep is necessary in order to ensure that + # sorting by last_activity_at works correctly when running + # in Jenkins. + time.sleep(time_between_creation) teams.append(json.loads(response.text)) return teams @@ -107,15 +114,8 @@ class TeamsTabBase(UniqueCourseTest): self.assertEqual(expected_team['name'], team_card_name) self.assertEqual(expected_team['description'], team_card_description) - team_cards = page.team_cards - team_card_names = [ - team_card.find_element_by_css_selector('.card-title').text - for team_card in team_cards.results - ] - team_card_descriptions = [ - team_card.find_element_by_css_selector('.card-description').text - for team_card in team_cards.results - ] + team_card_names = page.team_names + team_card_descriptions = page.team_descriptions map(assert_team_equal, expected_teams, team_card_names, team_card_descriptions) def verify_my_team_count(self, expected_number_of_teams): @@ -124,6 +124,10 @@ class TeamsTabBase(UniqueCourseTest): # We are doing these operations on this top-level page object to avoid reloading the page. self.teams_page.verify_my_team_count(expected_number_of_teams) + def only_team_events(self, event): + """Filter out all non-team events.""" + return event['event_type'].startswith('edx.team.') + @ddt.ddt @attr('shard_5') @@ -445,7 +449,7 @@ class BrowseTopicsTest(TeamsTabBase): {u"max_team_size": 1, u"topics": [{"name": "", "id": "", "description": initial_description}]} ) self.topics_page.visit() - truncated_description = self.topics_page.topic_cards[0].text + truncated_description = self.topics_page.topic_descriptions[0] self.assertLess(len(truncated_description), len(initial_description)) self.assertTrue(truncated_description.endswith('...')) self.assertIn(truncated_description.split('...')[0], initial_description) @@ -468,11 +472,12 @@ class BrowseTopicsTest(TeamsTabBase): self.topics_page.browse_teams_for_topic('Example Topic') browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, topic) self.assertTrue(browse_teams_page.is_browser_on_page()) - self.assertEqual(browse_teams_page.header_topic_name, 'Example Topic') - self.assertEqual(browse_teams_page.header_topic_description, 'Description') + self.assertEqual(browse_teams_page.header_name, 'Example Topic') + self.assertEqual(browse_teams_page.header_description, 'Description') @attr('shard_5') +@ddt.ddt class BrowseTeamsWithinTopicTest(TeamsTabBase): """ Tests for browsing Teams within a Topic on the Teams page. @@ -482,21 +487,45 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): def setUp(self): super(BrowseTeamsWithinTopicTest, self).setUp() self.topic = {u"name": u"Example Topic", u"id": "example_topic", u"description": "Description"} - self.set_team_configuration({'course_id': self.course_id, 'max_team_size': 10, 'topics': [self.topic]}) + self.max_team_size = 10 + self.set_team_configuration({ + 'course_id': self.course_id, + 'max_team_size': self.max_team_size, + 'topics': [self.topic] + }) self.browse_teams_page = BrowseTeamsPage(self.browser, self.course_id, self.topic) self.topics_page = BrowseTopicsPage(self.browser, self.course_id) + def teams_with_default_sort_order(self, teams): + """Return a list of teams sorted according to the default ordering + (last_activity_at, with a secondary sort by open slots). + """ + return sorted( + sorted(teams, key=lambda t: len(t['membership']), reverse=True), + key=lambda t: parse(t['last_activity_at']).replace(microsecond=0), + reverse=True + ) + def verify_page_header(self): """Verify that the page header correctly reflects the current topic's name and description.""" - self.assertEqual(self.browse_teams_page.header_topic_name, self.topic['name']) - self.assertEqual(self.browse_teams_page.header_topic_description, self.topic['description']) + self.assertEqual(self.browse_teams_page.header_name, self.topic['name']) + self.assertEqual(self.browse_teams_page.header_description, self.topic['description']) - def verify_on_page(self, page_num, total_teams, pagination_header_text, footer_visible): + def verify_search_header(self, search_results_page, search_query): + """Verify that the page header correctly reflects the current topic's name and description.""" + self.assertEqual(search_results_page.header_name, 'Team Search') + self.assertEqual( + search_results_page.header_description, + 'Showing results for "{search_query}"'.format(search_query=search_query) + ) + + def verify_on_page(self, teams_page, page_num, total_teams, pagination_header_text, footer_visible): """ Verify that we are on the correct team list page. Arguments: - page_num (int): The one-indexed page we expect to be on + teams_page (BaseTeamsPage): The teams page object that should be the current page. + page_num (int): The one-indexed page number that we expect to be on total_teams (list): An unsorted list of all the teams for the current topic pagination_header_text (str): Text we expect to see in the @@ -504,18 +533,75 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): footer_visible (bool): Whether we expect to see the pagination footer controls. """ - alphabetized_teams = sorted(total_teams, key=lambda team: team['name']) - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), pagination_header_text) + sorted_teams = self.teams_with_default_sort_order(total_teams) + self.assertTrue(teams_page.get_pagination_header_text().startswith(pagination_header_text)) self.verify_teams( - self.browse_teams_page, - alphabetized_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE] + teams_page, + sorted_teams[(page_num - 1) * self.TEAMS_PAGE_SIZE:page_num * self.TEAMS_PAGE_SIZE] ) self.assertEqual( - self.browse_teams_page.pagination_controls_visible(), + teams_page.pagination_controls_visible(), footer_visible, msg='Expected paging footer to be ' + 'visible' if footer_visible else 'invisible' ) + @ddt.data( + ('open_slots', 'last_activity_at', True), + ('last_activity_at', 'open_slots', True) + ) + @ddt.unpack + def test_sort_teams(self, sort_order, secondary_sort_order, reverse): + """ + Scenario: the user should be able to sort the list of teams by open slots or last activity + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse teams within a topic + Then I should see a list of teams for that topic + When I choose a sort order + Then I should see the paginated list of teams in that order + """ + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + for i, team in enumerate(random.sample(teams, len(teams))): + for _ in range(i): + user_info = AutoAuthPage(self.browser, course_id=self.course_id).visit().user_info + self.create_membership(user_info['username'], team['id']) + team['open_slots'] = self.max_team_size - i + # Parse last activity date, removing microseconds because + # the Django ORM does not support them. Will be fixed in + # Django 1.8. + team['last_activity_at'] = parse(team['last_activity_at']).replace(microsecond=0) + # Re-authenticate as staff after creating users + AutoAuthPage( + self.browser, + course_id=self.course_id, + staff=True + ).visit() + self.browse_teams_page.visit() + self.browse_teams_page.sort_teams_by(sort_order) + team_names = self.browse_teams_page.team_names + self.assertEqual(len(team_names), self.TEAMS_PAGE_SIZE) + sorted_teams = [ + team['name'] + for team in sorted( + sorted(teams, key=lambda t: t[secondary_sort_order], reverse=reverse), + key=lambda t: t[sort_order], + reverse=reverse + ) + ][:self.TEAMS_PAGE_SIZE] + self.assertEqual(team_names, sorted_teams) + + def test_default_sort_order(self): + """ + Scenario: the list of teams should be sorted by last activity by default + Given I am enrolled in a course with team configuration and topics + When I visit the Teams page + And I browse teams within a topic + Then I should see a list of teams for that topic, sorted by last activity + """ + self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + self.browse_teams_page.visit() + self.assertEqual(self.browse_teams_page.sort_order, 'last activity') + def test_no_teams(self): """ Scenario: Visiting a topic with no teams should not display any teams. @@ -529,7 +615,7 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): """ self.browse_teams_page.visit() self.verify_page_header() - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.assertEqual(len(self.browse_teams_page.team_cards), 0, msg='Expected to see no team cards') self.assertFalse( self.browse_teams_page.pagination_controls_visible(), @@ -548,10 +634,12 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): And I should see a button to add a team And I should not see a pagination footer """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE) + teams = self.teams_with_default_sort_order( + self.create_teams(self.topic, self.TEAMS_PAGE_SIZE, time_between_creation=1) + ) self.browse_teams_page.visit() self.verify_page_header() - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 1-10 out of 10 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 1-10 out of 10 total')) self.verify_teams(self.browse_teams_page, teams) self.assertFalse( self.browse_teams_page.pagination_controls_visible(), @@ -571,14 +659,14 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): And when I click on the previous page button Then I should see that I am on the first page of results """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1) + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 1, time_between_creation=1) self.browse_teams_page.visit() self.verify_page_header() - self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True) + self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True) self.browse_teams_page.press_next_page_button() - self.verify_on_page(2, teams, 'Showing 11-11 out of 11 total', True) + self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-11 out of 11 total', True) self.browse_teams_page.press_previous_page_button() - self.verify_on_page(1, teams, 'Showing 1-10 out of 11 total', True) + self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 11 total', True) def test_teams_page_input(self): """ @@ -593,28 +681,24 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): When I input the first page Then I should see that I am on the first page of results """ - teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10) + teams = self.create_teams(self.topic, self.TEAMS_PAGE_SIZE + 10, time_between_creation=1) self.browse_teams_page.visit() self.verify_page_header() - self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True) + self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True) self.browse_teams_page.go_to_page(2) - self.verify_on_page(2, teams, 'Showing 11-20 out of 20 total', True) + self.verify_on_page(self.browse_teams_page, 2, teams, 'Showing 11-20 out of 20 total', True) self.browse_teams_page.go_to_page(1) - self.verify_on_page(1, teams, 'Showing 1-10 out of 20 total', True) + self.verify_on_page(self.browse_teams_page, 1, teams, 'Showing 1-10 out of 20 total', True) - def test_navigation_links(self): + def test_browse_team_topics(self): """ Scenario: User should be able to navigate to "browse all teams" and "search team description" links. - Given I am enrolled in a course with a team configuration and a topic - containing one team - When I visit the Teams page for that topic + Given I am enrolled in a course with teams enabled + When I visit the Teams page for a topic Then I should see the correct page header - And I should see the link to "browse all team" - And I should navigate to that link - And I see the relevant page loaded - And I should see the link to "search teams" - And I should navigate to that link - And I see the relevant page loaded + And I should see the link to "browse teams in other topics" + When I should navigate to that link + Then I should see the topic browse page """ self.browse_teams_page.visit() self.verify_page_header() @@ -622,10 +706,24 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): self.browse_teams_page.click_browse_all_teams_link() self.assertTrue(self.topics_page.is_browser_on_page()) + @skip('Disabled until search connectivity issues are resolved, see TNL-3206') + def test_search(self): + """ + Scenario: User should be able to search for a team + Given I am enrolled in a course with teams enabled + When I visit the Teams page for that topic + And I search for 'banana' + Then I should see the search result page + And the search header should be shown + And 0 results should be shown + """ + # Note: all searches will return 0 results with the mock search server + # used by Bok Choy. + self.create_teams(self.topic, 5) self.browse_teams_page.visit() - self.verify_page_header() - self.browse_teams_page.click_search_team_link() - # TODO Add search page expectation once that implemented. + search_results_page = self.browse_teams_page.search('banana') + self.verify_search_header(search_results_page, 'banana') + self.assertTrue(search_results_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) @attr('shard_5') @@ -652,8 +750,8 @@ class TeamFormActions(TeamsTabBase): self.browse_teams_page.click_create_team_link() self.verify_page_header( title='Create a New Team', - description='Create a new team if you can\'t find existing teams to ' - 'join, or if you would like to learn with friends you know.', + description='Create a new team if you can\'t find an existing team to join, ' + 'or if you would like to learn with friends you know.', breadcrumbs='All Topics {topic_name}'.format(topic_name=self.topic['name']) ) @@ -807,7 +905,8 @@ class CreateTeamTest(TeamFormActions): Then I should see the Create Team header and form When I fill all the fields present with appropriate data And I click Create button - Then I should see the page for my team + Then I expect analytics events to be emitted + And I should see the page for my team And I should see the message that says "You are member of this team" And the new team should be added to the list of teams within the topic And the number of teams should be updated on the topic card @@ -819,7 +918,24 @@ class CreateTeamTest(TeamFormActions): self.verify_and_navigate_to_create_team_page() self.fill_create_or_edit_form() - self.create_or_edit_team_page.submit_form() + + expected_events = [ + { + 'event_type': 'edx.team.created', + 'event': { + 'course_id': self.course_id, + } + }, + { + 'event_type': 'edx.team.learner_added', + 'event': { + 'course_id': self.course_id, + 'add_method': 'added_on_create', + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.create_or_edit_team_page.submit_form() # Verify that the page is shown for the new team team_page = TeamPage(self.browser, self.course_id) @@ -848,13 +964,13 @@ class CreateTeamTest(TeamFormActions): Then I should see teams list page without any new team. And if I switch to "My Team", it shows no teams """ - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.verify_and_navigate_to_create_team_page() self.create_or_edit_team_page.cancel_team() self.assertTrue(self.browse_teams_page.is_browser_on_page()) - self.assertEqual(self.browse_teams_page.get_pagination_header_text(), 'Showing 0 out of 0 total') + self.assertTrue(self.browse_teams_page.get_pagination_header_text().startswith('Showing 0 out of 0 total')) self.teams_page.click_all_topics() self.teams_page.verify_team_count_in_first_topic(0) @@ -1238,6 +1354,7 @@ class TeamPageTest(TeamsTabBase): And I should not see New Post button When I click on Join Team button Then there should be no Join Team button and no message + And an analytics event should be emitted And I should see the updated information under Team Details And I should see New Post button And if I switch to "My Team", the team I have joined is displayed @@ -1245,7 +1362,17 @@ class TeamPageTest(TeamsTabBase): self._set_team_configuration_and_membership(create_membership=False) self.team_page.visit() self.assertTrue(self.team_page.join_team_button_present) - self.team_page.click_join_team_button() + expected_events = [ + { + 'event_type': 'edx.team.learner_added', + 'event': { + 'course_id': self.course_id, + 'add_method': 'joined_from_team_view' + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.team_page.click_join_team_button() self.assertFalse(self.team_page.join_team_button_present) self.assertFalse(self.team_page.join_team_message_present) self.assert_team_details(num_members=1, is_member=True) @@ -1305,6 +1432,7 @@ class TeamPageTest(TeamsTabBase): Then I should see Leave Team link When I click on Leave Team link Then user should be removed from team + And an analytics event should be emitted And I should see Join Team button And I should not see New Post button And if I switch to "My Team", the team I have left is not displayed @@ -1313,7 +1441,17 @@ class TeamPageTest(TeamsTabBase): self.team_page.visit() self.assertFalse(self.team_page.join_team_button_present) self.assert_team_details(num_members=1) - self.team_page.click_leave_team_link() + expected_events = [ + { + 'event_type': 'edx.team.learner_removed', + 'event': { + 'course_id': self.course_id, + 'remove_method': 'self_removal' + } + } + ] + with self.assert_events_match_during(event_filter=self.only_team_events, expected_events=expected_events): + self.team_page.click_leave_team_link() self.assert_team_details(num_members=0, is_member=False) self.assertTrue(self.team_page.join_team_button_present) diff --git a/common/test/acceptance/tests/studio/test_studio_asset.py b/common/test/acceptance/tests/studio/test_studio_asset.py index aa65420359..0ae1b60ce2 100644 --- a/common/test/acceptance/tests/studio/test_studio_asset.py +++ b/common/test/acceptance/tests/studio/test_studio_asset.py @@ -2,6 +2,8 @@ Acceptance tests for Studio related to the asset index page. """ +from flaky import flaky + from ...pages.studio.asset_index import AssetIndexPage from .base_studio_test import StudioCourseTest @@ -35,6 +37,7 @@ class AssetIndexTest(StudioCourseTest): """ self.asset_page.visit() + @flaky # TODO fix this, see SOL-1160 def test_type_filter_exists(self): """ Make sure type filter is on the page. diff --git a/common/test/acceptance/tests/studio/test_studio_library.py b/common/test/acceptance/tests/studio/test_studio_library.py index 3fbb58d6a6..ca3f0a0150 100644 --- a/common/test/acceptance/tests/studio/test_studio_library.py +++ b/common/test/acceptance/tests/studio/test_studio_library.py @@ -522,9 +522,7 @@ class LibraryUsersPageTest(StudioLibraryTest): """ self.page = LibraryUsersPage(self.browser, self.library_key) self.page.visit() - self.page.wait_until_no_loading_indicator() - @flaky # TODO fix this; see TNL-2647 def test_user_management(self): """ Scenario: Ensure that we can edit the permissions of users. diff --git a/common/test/acceptance/tests/studio/test_studio_settings_certificates.py b/common/test/acceptance/tests/studio/test_studio_settings_certificates.py index 76986d040a..ca1e6da447 100644 --- a/common/test/acceptance/tests/studio/test_studio_settings_certificates.py +++ b/common/test/acceptance/tests/studio/test_studio_settings_certificates.py @@ -170,6 +170,8 @@ class CertificatesTest(StudioCourseTest): self.assertEqual(len(self.certificates_page.certificates), 1) + #Refreshing the page, So page have the updated certificate object. + self.certificates_page.refresh() signatory = self.certificates_page.certificates[0].signatories[0] self.assertIn("Updated signatory name", signatory.name) self.assertIn("Update signatory title", signatory.title) diff --git a/common/test/acceptance/tests/video/test_video_events.py b/common/test/acceptance/tests/video/test_video_events.py index 629fb6db06..158e01a0f2 100644 --- a/common/test/acceptance/tests/video/test_video_events.py +++ b/common/test/acceptance/tests/video/test_video_events.py @@ -3,6 +3,7 @@ import datetime import json import ddt +import unittest from ..helpers import EventsTestMixin from .test_video_module import VideoBaseTest @@ -60,6 +61,7 @@ class VideoEventsTestMixin(EventsTestMixin, VideoBaseTest): class VideoEventsTest(VideoEventsTestMixin): """ Test video player event emission """ + @unittest.skip('AN-5867') def test_video_control_events(self): """ Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources diff --git a/conf/locale/ar/LC_MESSAGES/django.mo b/conf/locale/ar/LC_MESSAGES/django.mo index e45d076fda..3f9b21e4f6 100644 Binary files a/conf/locale/ar/LC_MESSAGES/django.mo and b/conf/locale/ar/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ar/LC_MESSAGES/django.po b/conf/locale/ar/LC_MESSAGES/django.po index d942eb2e4e..89fec36537 100644 --- a/conf/locale/ar/LC_MESSAGES/django.po +++ b/conf/locale/ar/LC_MESSAGES/django.po @@ -129,7 +129,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-08-12 08:13+0000\n" "Last-Translator: Ahmed Jazzar \n" "Language-Team: Arabic (http://www.transifex.com/open-edx/edx-platform/language/ar/)\n" @@ -1381,10 +1381,6 @@ msgstr "صحيح" msgid "incorrect" msgstr "غير صحيح" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "ناقص" @@ -1407,10 +1403,6 @@ msgstr "هذا صحيح" msgid "This is incorrect." msgstr "هذا خطأ" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "هذا غير مجاب عليه" @@ -5190,8 +5182,15 @@ msgid "{month} {day}, {year}" msgstr "{month} {day}، {year}" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." -msgstr "مساق دروس يقدِّمه {partner_name}، عبر {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." +msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by #. students on this platform @@ -5302,16 +5301,14 @@ msgstr "تُقِرّ {platform_name} بإنجازات الطالب التالي #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" -"هذه شهادة صالحة من {platform_name} للمستخدم {user_name} الذي شارك في " -"{partner_name} {course_number}" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" -msgstr "شهادة {partner_name} {course_number} | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" +msgstr "" #. Translators: This text fragment appears after the student's name #. (displayed in a large font) on the certificate @@ -5478,6 +5475,14 @@ msgstr "" "إذا لم يظهر مساقك على لوحة معلوماتك، يُرجى الاتصال بـ " "{payment_support_link}." +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -6032,6 +6037,23 @@ msgstr "اسم المستخدم {user} موجود مسبقًا." msgid "File is not attached." msgstr "الملف غير مُرفَق. " +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr " الفاتورة رقم '{num}' غير موجودة." @@ -6451,6 +6473,10 @@ msgstr "لا توجد وضعية للمساق CourseMode باسم ({mode_slug}) msgid "CourseMode price updated successfully" msgstr "تمّ عملية تحديث سعر CourseMode بنجاح" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "تتوافر بيانات التسجيل الآن على {dashboard_link}." @@ -6557,18 +6583,6 @@ msgstr "بريد إلكتروني خارجي" msgid "Grades for assignment \"{name}\"" msgstr "درجات الواجب \"{name}\"" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "وُجدت {num} سجلّات للتخلّص منها." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "نأسف لتعذّر إيجاد وحدة بذلك الرابط." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "حالة الطالب بالنسبة للمسألة {problem}" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "الدرجات من المساق رقم {course_id}" @@ -6736,6 +6750,12 @@ msgstr "جرى الحذف" msgid "emailed" msgstr "الإرسال بالبريد الإلكتروني" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "الاستحداث" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6748,12 +6768,6 @@ msgstr "التقييم " msgid "problem distribution graded" msgstr "تقييم توزيع المسائل " -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "الاستحداث" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -8382,12 +8396,12 @@ msgid "course_id must be provided" msgstr "يجب توفير الرقم التغريفي للمساق course_id." #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" -msgstr "الرقم التعريفي الذي جرى توفيره حول الموضوع {topic_id} غير صالح." +msgid "text_search and order_by cannot be provided together" +msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." -msgstr "إنّ خاصّية البحث في النصّ text_search غير مدعومة بعد." +msgid "The supplied topic id {topic_id} is not valid" +msgstr "الرقم التعريفي الذي جرى توفيره حول الموضوع {topic_id} غير صالح." #. Translators: 'ordering' is a string describing a way #. of ordering a list. For example, {ordering} may be @@ -10195,6 +10209,10 @@ msgstr "المساعدة" msgid "Sign Out" msgstr "تسجيل الخروج" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "جميع الحقوق محفوظة" @@ -12862,8 +12880,10 @@ msgid "Section:" msgstr "القسم:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "اسم الرابط urlname الخاص بالمسألة: " +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -14860,6 +14880,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -17396,60 +17430,51 @@ msgid "This module is not enabled." msgstr "هذه الوحدة غير مفعَّلة." #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" -"عند استكمال الطلّاب لمساقك بنجاح، يحصلون على شهادة تُثبِت إنجازهم. وإذا كنت " -"عضوًا في فريق المساق بدور مشرِف في الاستوديو، يمكنك إعداد شهادة مساقك." #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" -"يُرجى النقر على {em_start}أَضِف شهادتك الأولى{em_end} لتُضيف إعدادات " -"الشهادة. ثمّ حَمِّل شعار المؤسّسة لوضعه على الشهادة وحدِّد موقِّعًا واحدًا " -"على الأقلّ وأربعة على الأكثر لكل شهادة. ويمكنك أيضًا تحميل صورة التوقيع لكل " -"موقّع. {em_start}ملاحظة:{em_end} تُستخدَم صور التواقيع فقط للشهادات " -"الموثَّقة. أو إذا أردت، يمكنك أن تحدِّد اسمًا مختلفًا للمساق لوضعه على " -"شهادتك. وقد ترغب في استخدام اسم مختلف إذا كان، مثلًا، الاسم الرسمي للمساق " -"طويلًا جدًّا إلى درجة أنّه لن يبدو واضحًا على الشهادة." #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" -"يُرجى اختيار وضعية للمساق ثمّ النقر على {em_start}معاينة الشهادة{em_end} " -"لمعاينة الشهادة التي سيحصل عليها المتعلّم في مسار التسجيل المُنتقى. وعندما " -"تَجهَز الشهادة للإصدار، يُرجى النقر على {em_start}تفعيل.{em_end}. ولإيقاف " -"إصدار شهادة مفعَّلة، يُرجى النقر على {em_start}إبطال التفعيل{em_end}." #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" -"لتعديل إعدادات الشهادة، يُرجى تحريك الماوس إلى الزاوية العليا اليمنى من " -"الاستمارة والنقر على {em_start}تعديل{em_end}. ولحذف إحدى الشهادات، يُرجى " -"تحريك الماوس إلى الزاوية العليا اليمنى من الاستمارة والنقر على رمز الحذف. " -"وبشكل عام، لا تحذف الشهادات بعد بدء المساق، لأنّ بعض الشهادات قد تكون " -"أُصدِرت مسبقًا للمتعلِّمين." #: cms/templates/certificates.html msgid "Learn more about certificates" diff --git a/conf/locale/ar/LC_MESSAGES/djangojs.mo b/conf/locale/ar/LC_MESSAGES/djangojs.mo index 5dc68dd392..a08f9c7506 100644 Binary files a/conf/locale/ar/LC_MESSAGES/djangojs.mo and b/conf/locale/ar/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ar/LC_MESSAGES/djangojs.po b/conf/locale/ar/LC_MESSAGES/djangojs.po index 9aa50546e7..d42511a73a 100644 --- a/conf/locale/ar/LC_MESSAGES/djangojs.po +++ b/conf/locale/ar/LC_MESSAGES/djangojs.po @@ -57,6 +57,7 @@ # Translators: # Abdelghani Gadiri , 2014 # qrfahasan , 2014 +# Ahmad , 2015 # Ahmed Jazzar , 2015 # mohammad hamdi , 2014 # may , 2014 @@ -80,8 +81,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Arabic (http://www.transifex.com/open-edx/edx-platform/language/ar/)\n" "MIME-Version: 1.0\n" @@ -127,8 +128,8 @@ msgstr "موافق" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -139,6 +140,11 @@ msgstr "موافق" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "إلغاء" @@ -148,17 +154,6 @@ msgstr "إلغاء" #: cms/static/js/views/manage_users_and_roles.js #: cms/static/js/views/show_textbook.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/course-outline.underscore -#: cms/templates/js/course_grade_policy.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/group-configuration-editor.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-editor.underscore -#: cms/templates/js/xblock-outline.underscore msgid "Delete" msgstr "حذف" @@ -233,14 +228,12 @@ msgstr "خطأ" msgid "Save" msgstr "حفظ" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: cms/static/js/views/modals/edit_xblock.js #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-editor.underscore msgid "Close" -msgstr "إغلاق" +msgstr "إغلاق " #: common/lib/xmodule/xmodule/js/src/annotatable/display.js msgid "Show Annotations" @@ -336,6 +329,8 @@ msgstr "لم يلبِّ مجموع نقاطك المعيار المطلوب لل #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "تقديم" @@ -765,18 +760,10 @@ msgstr "خصائص المستند" msgid "Edit HTML" msgstr "تعديل لغة HTML" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/course_info_handouts.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/xblock-string-field-editor.underscore msgid "Edit" msgstr "تعديل" @@ -1177,11 +1164,9 @@ msgstr "مستند جديد" msgid "New window" msgstr "نافذة جديدة" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/paging-header.underscore msgid "Next" msgstr "التالي" @@ -1531,12 +1516,9 @@ msgstr "" "يبدو أن الرابط الذي أدخلته عبارة عن رابط خارجي، هل تريد إضافة البادئة " "http:// اللازمة؟" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/signatory-editor.underscore msgid "Title" msgstr "العنوان" @@ -2161,6 +2143,18 @@ msgstr "نأسف لحدوث مشكلة في حذف هذا التعليق. يُر msgid "Are you sure you want to delete this response?" msgstr "هل أنت واثق من أنّك تودّ حذف هذا الرد؟" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "إظهار %(first_index)s من أصل %(num_items)s." @@ -2360,6 +2354,7 @@ msgstr "تاريخ النشر" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2439,21 +2434,34 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "الاسم " -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "open_slots" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "عدد أعضاء الفريق" +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "إنشاء " + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2478,6 +2486,7 @@ msgid "Language" msgstr "اللغة" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2488,6 +2497,7 @@ msgid "Country" msgstr "البلد" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2520,20 +2530,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "مشاهدة %(span_start)s %(team_name)s %(span_end)s" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "تأكيد" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2555,6 +2593,10 @@ msgstr "" msgid "teams" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "الفِرَق" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" @@ -2563,31 +2605,52 @@ msgstr "" "تفضّل بالاطّلاع على جميع الفرق في مساقك، مرتّبةً بحسب الموضوع. وانضم إلى " "أحدها للتعاون مع المتعلّمين الآخرين المهتمّين بالمجال نفسه مثلك." -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "الفِرَق" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2614,15 +2677,27 @@ msgstr[3] "%(team_count)s فِرق" msgstr[4] "%(team_count)s فِرق" msgstr[5] "%(team_count)s فريقاً" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "استعراض الفِرق في موضوع %(topic_name)s" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -4098,11 +4173,6 @@ msgstr "التقاط صورة لبطاقتك الشخصية" msgid "Review your info" msgstr "مراجعة معلوماتك" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "تأكيد" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "نأسف لحدوث خطأ. يُرجى محاولة إعادة تحميل الصفحة." @@ -4690,7 +4760,7 @@ msgstr "جرى حذف ملفّك." msgid "Date Added" msgstr "تاريخ الإضافة " -#: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: cms/static/js/views/assets.js msgid "Type" msgstr "النوع" @@ -5304,18 +5374,6 @@ msgstr "" "لا يمكن أن يزيد مجموع الأحرف في حقلي المؤسسة ورمز المكتبة عن <%=limit%> " "حرفًا." -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "حقل مطلوب." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "يُرجى عدم إدخال أي مسافات في هذا الحقل." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "يُرجى عدم إدخال أي مسافات أو أحرف خاصة في هذا الحقل." - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "مكوِّن" @@ -5408,11 +5466,526 @@ msgstr "الإجراءات" msgid "Due Date" msgstr "تاريخ الاستحقاق" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "الحالة" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "gettext(" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "تبرَّع" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5453,12 +6026,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "gettext(" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5554,10 +6121,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "تبرَّع" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "اعرف المزيد" @@ -6566,6 +7129,16 @@ msgstr "استخدم خاصية السحب والإفلات أو اضغط هنا msgid "status" msgstr "الحالة" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "المسائل الشائعة" @@ -6640,6 +7213,11 @@ msgstr "الرمز التعريفي" msgid "Certificate Details" msgstr "تفاصيل الشهادة" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6686,19 +7264,13 @@ msgstr "" "الحقل فارغًا لاستخدام العنوان الرسمي للمساق." #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" -msgstr "إضافة مُوَقّع" +msgid "Add Additional Signatory" +msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "(يمكن إضافة 4 موقِّعين كحد أقصى لكل شهادة)" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "إنشاء " - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "اختر الوضع" @@ -7132,10 +7704,6 @@ msgstr "لم تُضِف بعد أي كتب إلى هذا المساق." msgid "Add your first textbook" msgstr "أضِف أوّل كتاب لك." -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "السابق" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "تحميلات سابقة" diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo index cf1235427e..573ab2711b 100644 Binary files a/conf/locale/eo/LC_MESSAGES/django.mo and b/conf/locale/eo/LC_MESSAGES/django.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/django.po b/conf/locale/eo/LC_MESSAGES/django.po index 61e51c5a26..30f72b81fd 100644 --- a/conf/locale/eo/LC_MESSAGES/django.po +++ b/conf/locale/eo/LC_MESSAGES/django.po @@ -37,8 +37,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-24 22:03+0000\n" -"PO-Revision-Date: 2015-08-24 22:03:48.240461\n" +"POT-Creation-Date: 2015-09-04 14:15+0000\n" +"PO-Revision-Date: 2015-09-04 14:15:47.324123\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -1418,10 +1418,6 @@ msgstr "çörréçt Ⱡ'σяєм ιρѕυм #" msgid "incorrect" msgstr "ïnçörréçt Ⱡ'σяєм ιρѕυм ∂σł#" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "pärtïällý çörréçt Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "ïnçömplété Ⱡ'σяєм ιρѕυм ∂σłσ#" @@ -1444,10 +1440,6 @@ msgstr "Thïs ïs çörréçt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" msgid "This is incorrect." msgstr "Thïs ïs ïnçörréçt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "Thïs ïs pärtïällý çörréçt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "Thïs ïs ünänswéréd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" @@ -5858,10 +5850,20 @@ msgid "{month} {day}, {year}" msgstr "{month} {day}, {year} Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." msgstr "" -"ä çöürsé öf stüdý öfféréd ßý {partner_name}, thröügh {platform_name}. Ⱡ'σяєм" -" ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" +"ä çöürsé öf stüdý öfféréd ßý {partner_short_name}, än önlïné léärnïng " +"ïnïtïätïvé öf {partner_long_name} thröügh {platform_name}. Ⱡ'σяєм ιρѕυм " +"∂σłσя ѕιт αмєт, ¢σηѕє¢т#" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." +msgstr "" +"ä çöürsé öf stüdý öfféréd ßý {partner_short_name}, thröügh {platform_name}. " +"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" #. Translators: Accomplishments describe the awards/certifications obtained by #. students on this platform @@ -5995,19 +5997,19 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" "Thïs ïs ä välïd {platform_name} çértïfïçäté för {user_name}, whö " -"pärtïçïpätéd ïn {partner_name} {course_number} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя #" +"pärtïçïpätéd ïn {partner_short_name} {course_number} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " +"αмєт, ¢σηѕє¢тєтυя #" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" -"{partner_name} {course_number} Çértïfïçäté | {platform_name} Ⱡ'σяєм ιρѕυм " -"∂σłσя ѕιт αмєт, ¢σηѕ#" +"{partner_short_name} {course_number} Çértïfïçäté | {platform_name} Ⱡ'σяєм " +"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" #. Translators: This text fragment appears after the student's name #. (displayed in a large font) on the certificate @@ -6194,6 +6196,15 @@ msgstr "" "Ìf ýöür çöürsé döés nöt äppéär ön ýöür däshßöärd, çöntäçt " "{payment_support_link}. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" +"{course_id} ïs nöt ä välïd çöürsé kéý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "Çöürsé {course_id} döés nöt éxïst. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -6804,6 +6815,35 @@ msgstr "Ûsérnämé {user} älréädý éxïsts. Ⱡ'σяєм ιρѕυм ∂σ msgid "File is not attached." msgstr "Fïlé ïs nöt ättäçhéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" +"Çöüld nöt fïnd prößlém wïth thïs löçätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" +"Thé prößlém réspönsés répört ïs ßéïng çréätéd. Tö vïéw thé stätüs öf thé " +"répört, séé Péndïng Täsks ßélöw. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт #" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" +"À prößlém réspönsés répört générätïön täsk ïs älréädý ïn prögréss. Çhéçk thé" +" 'Péndïng Täsks' täßlé för thé stätüs öf thé täsk. Whén çömplétéd, thé " +"répört wïll ßé äväïläßlé för döwnlöäd ïn thé täßlé ßélöw. Ⱡ'σяєм ιρѕυм ∂σłσя" +" ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт" +" łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ " +"єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ " +"αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ " +"ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢#" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -7334,6 +7374,10 @@ msgstr "" "ÇöürséMödé prïçé üpdätéd süççéssfüllý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυ#" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "Nö énd däté sét Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -7439,21 +7483,6 @@ msgstr "Éxtérnäl émäïl Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" msgid "Grades for assignment \"{name}\"" msgstr "Grädés för ässïgnmént \"{name}\" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "Föünd {num} réçörds tö dümp. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "" -"Çöüldn't fïnd mödülé wïth thät ürlnämé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя#" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "" -"Stüdént stäté för prößlém {problem} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "Grädés fröm {course_id} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" @@ -7656,6 +7685,12 @@ msgstr "délétéd Ⱡ'σяєм ιρѕυм #" msgid "emailed" msgstr "émäïléd Ⱡ'σяєм ιρѕυм #" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "générätéd Ⱡ'σяєм ιρѕυм ∂σł#" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7668,12 +7703,6 @@ msgstr "grädéd Ⱡ'σяєм ιρѕυ#" msgid "problem distribution graded" msgstr "prößlém dïstrïßütïön grädéd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "générätéd Ⱡ'σяєм ιρѕυм ∂σł#" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -10900,18 +10929,6 @@ msgstr "" "Nö dätä prövïdéd för üsér préférénçé üpdäté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя #" -#: openedx/core/lib/api/paginators.py -msgid "Page is not 'last', nor can it be converted to an int." -msgstr "" -"Pägé ïs nöt 'läst', nör çän ït ßé çönvértéd tö än ïnt. Ⱡ'σяєм ιρѕυм ∂σłσя " -"ѕιт αмєт, ¢σηѕє¢тєтυя α#" - -#: openedx/core/lib/api/paginators.py -#, python-format -msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "" -"Ìnvälïd pägé (%(page_number)s): %(message)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" - #: openedx/core/lib/api/view_utils.py msgid "This value is invalid." msgstr "Thïs välüé ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" @@ -14596,8 +14613,12 @@ msgid "Section:" msgstr "Séçtïön: Ⱡ'σяєм ιρѕυм ∂#" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "Prößlém ürlnämé: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" +"Tö döwnlöäd ä ÇSV lïstïng stüdént réspönsés tö ä gïvén prößlém, vïsït thé " +"Dätä Döwnlöäd séçtïön öf thé Ìnstrüçtör Däshßöärd. Ⱡ'σяєм ι#" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -16947,6 +16968,29 @@ msgstr "" "Généräté Pröçtöréd Éxäm Résülts Répört Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя#" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" +"Tö généräté ä ÇSV fïlé thät lïsts äll stüdént änswérs tö ä gïvén prößlém, " +"éntér thé löçätïön öf thé prößlém (fröm ïts Stäff Déßüg Ìnfö). Ⱡ'σяєм ιρѕυм " +"∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя " +"ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ " +"ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ " +"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє " +"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт " +"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ єѕт łαвσя#" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "Prößlém löçätïön: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" +"Döwnlöäd ä ÇSV öf prößlém réspönsés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.mo b/conf/locale/eo/LC_MESSAGES/djangojs.mo index 115ea26634..b542d20641 100644 Binary files a/conf/locale/eo/LC_MESSAGES/djangojs.mo and b/conf/locale/eo/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.po b/conf/locale/eo/LC_MESSAGES/djangojs.po index c4c100b72d..409a0dd024 100644 --- a/conf/locale/eo/LC_MESSAGES/djangojs.po +++ b/conf/locale/eo/LC_MESSAGES/djangojs.po @@ -26,8 +26,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-24 22:03+0000\n" -"PO-Revision-Date: 2015-08-24 22:03:48.948707\n" +"POT-Creation-Date: 2015-09-04 14:15+0000\n" +"PO-Revision-Date: 2015-09-04 14:15:47.640544\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -73,8 +73,8 @@ msgstr "ÖK Ⱡ'σя#" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -89,6 +89,7 @@ msgstr "ÖK Ⱡ'σя#" #: common/static/common/templates/discussion/response-comment-edit.underscore #: common/static/common/templates/discussion/thread-edit.underscore #: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "Çänçél Ⱡ'σяєм ιρѕυ#" @@ -2160,6 +2161,22 @@ msgstr "" "Àré ýöü süré ýöü wänt tö délété thïs réspönsé? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя α#" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "Réqüïréd fïéld. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" +"Pléäsé dö nöt üsé äný späçés ïn thïs fïéld. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυя #" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" +"Pléäsé dö nöt üsé äný späçés ör spéçïäl çhäräçtérs ïn thïs fïéld. Ⱡ'σяєм " +"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "" @@ -2365,6 +2382,7 @@ msgid "Public" msgstr "Püßlïç Ⱡ'σяєм ιρѕυ#" #: common/static/js/vendor/ova/catch/js/catch.js +#: common/static/common/templates/components/search-field.underscore #: lms/djangoapps/support/static/support/templates/certificates.underscore msgid "Search" msgstr "Séärçh Ⱡ'σяєм ιρѕυ#" @@ -2429,21 +2447,35 @@ msgstr "" "αмєт, ¢σηѕє¢тєтυя α#" #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "läst äçtïvïtý Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "öpén slöts Ⱡ'σяєм ιρѕυм ∂σłσ#" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "nämé Ⱡ'σяєм ι#" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "öpén_slöts Ⱡ'σяєм ιρѕυм ∂σłσ#" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "téäm çöünt Ⱡ'σяєм ιρѕυм ∂σłσ#" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "Çréäté Ⱡ'σяєм ιρѕυ#" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "Ûpdäté Ⱡ'σяєм ιρѕυ#" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "Téäm Nämé (Réqüïréd) * Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" @@ -2530,22 +2562,52 @@ msgstr "" "Ýöü äré nöt çürréntlý ä mémßér öf äný téäm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя #" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "änd öthérs Ⱡ'σяєм ιρѕυм ∂σłσ#" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "Läst Àçtïvïtý %(date)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" "Vïéw %(span_start)s %(team_name)s %(span_end)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "Àn érrör öççürréd. Trý ägäïn. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "Léävé thïs téäm? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" +"Ìf ýöü léävé, ýöü çän nö löngér pöst ïn thïs téäm's dïsçüssïöns. Ýöür pläçé " +"wïll ßé äväïläßlé tö änöthér léärnér. Ⱡ'σяєм ιρѕυм ∂σłσ#" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Çönfïrm Ⱡ'σяєм ιρѕυм #" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" "Ýöü älréädý ßélöng tö änöthér téäm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "Thïs téäm ïs füll. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" @@ -2565,6 +2627,10 @@ msgstr "Àll téäms Ⱡ'σяєм ιρѕυм ∂σł#" msgid "teams" msgstr "téäms Ⱡ'σяєм ιρѕ#" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "Téäms Ⱡ'σяєм ιρѕ#" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" @@ -2579,33 +2645,57 @@ msgstr "" "¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт " "ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂#" -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "Téäms Ⱡ'σяєм ιρѕ#" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "Mý Téäm Ⱡ'σяєм ιρѕυм #" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "Bröwsé %(sr_start)s téäms %(sr_end)s Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "Téäm Séärçh Ⱡ'σяєм ιρѕυм ∂σłσя #" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" -"Çréäté ä néw téäm ïf ýöü çän't fïnd éxïstïng téäms tö jöïn, ör ïf ýöü wöüld " -"lïké tö léärn wïth frïénds ýöü knöw. Ⱡ'σяєм ιρѕυм ∂σłσя#" +"Shöwïng résülts för \"%(searchString)s\" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "Çréäté ä Néw Téäm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмє#" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" +"Çréäté ä néw téäm ïf ýöü çän't fïnd än éxïstïng téäm tö jöïn, ör ïf ýöü " +"wöüld lïké tö léärn wïth frïénds ýöü knöw. Ⱡ'σяєм ιρѕυм ∂σł#" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "Édït Téäm Ⱡ'σяєм ιρѕυм ∂σł#" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" +"Ìf ýöü mäké sïgnïfïçänt çhängés, mäké süré ýöü nötïfý mémßérs öf thé téäm " +"ßéföré mäkïng thésé çhängés. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "Séärçh téäms Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "Àll Töpïçs Ⱡ'σяєм ιρѕυм ∂σłσ#" @@ -2634,27 +2724,39 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "%(team_count)s Téäm Ⱡ'σяєм ιρѕυм ∂#" msgstr[1] "%(team_count)s Téäms Ⱡ'σяєм ιρѕυм ∂σł#" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "Töpïç Ⱡ'σяєм ιρѕ#" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" "Vïéw Téäms ïn thé %(topic_name)s Töpïç Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" -"Trý {browse_span_start}ßröwsïng äll téäms{span_end} ör " -"{search_span_start}séärçhïng téäm désçrïptïöns{span_end}. Ìf ýöü stïll çän't" -" fïnd ä téäm tö jöïn, {create_span_start}çréäté ä néw téäm ïn thïs " +"{browse_span_start}Bröwsé téäms ïn öthér töpïçs{span_end} ör " +"{search_span_start}séärçh téäms{span_end} ïn thïs töpïç. Ìf ýöü stïll çän't " +"fïnd ä téäm tö jöïn, {create_span_start}çréäté ä néw téäm ïn thïs " "töpïç{span_end}. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, " "ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм " "α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ " "єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє" " νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт " -"¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂#" +"¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αη#" #: lms/djangoapps/teams/static/teams/js/views/topics.js msgid "All topics" @@ -4343,11 +4445,6 @@ msgstr "Täké ä phötö öf ýöür ÌD Ⱡ'σяєм ιρѕυм ∂σłσя ѕ msgid "Review your info" msgstr "Révïéw ýöür ïnfö Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Çönfïrm Ⱡ'σяєм ιρѕυм #" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -5730,22 +5827,6 @@ msgstr "" "Thé çömßïnéd léngth öf thé örgänïzätïön änd lïßrärý çödé fïélds çännöt ßé " "möré thän <%=limit%> çhäräçtérs. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "Réqüïréd fïéld. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "" -"Pléäsé dö nöt üsé äný späçés ïn thïs fïéld. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢тєтυя #" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" -"Pléäsé dö nöt üsé äný späçés ör spéçïäl çhäräçtérs ïn thïs fïéld. Ⱡ'σяєм " -"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "çömpönént Ⱡ'σяєм ιρѕυм ∂σł#" @@ -5877,6 +5958,14 @@ msgstr "" "Éntér thé pägé nümßér ýöü'd lïké tö qüïçklý nävïgäté tö. Ⱡ'σяєм ιρѕυм ∂σłσя " "ѕιт αмєт, ¢σηѕє¢тєтυя α#" +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "Sörtéd ßý Ⱡ'σяєм ιρѕυм ∂σł#" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "Çléär séärçh Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + #: common/static/common/templates/discussion/discussion-home.underscore msgid "DISCUSSION HOME:" msgstr "DÌSÇÛSSÌÖN HÖMÉ: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" @@ -6296,8 +6385,12 @@ msgstr "" "Régénéräté thé üsér's çértïfïçäté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тє#" #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "Your team could not be created!" -msgstr "Ýöür téäm çöüld nöt ßé çréätéd! Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" +msgid "Your team could not be created." +msgstr "Ýöür téäm çöüld nöt ßé çréätéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "Ýöür téäm çöüld nöt ßé üpdätéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore msgid "" @@ -6327,15 +6420,20 @@ msgstr "" "¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρт#" #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "{primaryButtonTitle} {span_start}a new team{span_end}" -msgstr "" -"{primaryButtonTitle} {span_start}ä néw téäm{span_end} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" -" αмєт, #" +msgid "Create team." +msgstr "Çréäté téäm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "Cancel {span_start}a new team{span_end}" -msgstr "" -"Çänçél {span_start}ä néw téäm{span_end} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" +msgid "Update team." +msgstr "Ûpdäté téäm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "Çänçél téäm çréätïng. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "Çänçél téäm üpdätïng. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" #: lms/djangoapps/teams/static/teams/templates/team-actions.underscore msgid "Are you having trouble finding a team to join?" @@ -6343,7 +6441,7 @@ msgstr "" "Àré ýöü hävïng tröüßlé fïndïng ä téäm tö jöïn? Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя α#" -#: lms/djangoapps/teams/static/teams/templates/team-join.underscore +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore msgid "Join Team" msgstr "Jöïn Téäm Ⱡ'σяєм ιρѕυм ∂σł#" @@ -7683,6 +7781,16 @@ msgstr "" msgid "status" msgstr "stätüs Ⱡ'σяєм ιρѕυ#" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "Àdd Çömpönént: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "%(type)s Çömpönént Témpläté Ménü Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "Çömmön Prößlém Týpés Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" @@ -7761,6 +7869,11 @@ msgstr "ÌD Ⱡ'σя#" msgid "Certificate Details" msgstr "Çértïfïçäté Détäïls Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "Çöürsé Tïtlé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -7807,8 +7920,8 @@ msgstr "" "çértïfïçätés. Léävé ßlänk tö üsé thé öffïçïäl çöürsé tïtlé. Ⱡ'σяєм #" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" -msgstr "Àdd Sïgnätörý Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#" +msgid "Add Additional Signatory" +msgstr "Àdd Àddïtïönäl Sïgnätörý Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" @@ -7816,12 +7929,6 @@ msgstr "" "(Ûp tö 4 sïgnätörïés äré ällöwéd för ä çértïfïçäté) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "αмєт, ¢σηѕє¢тєтυя α#" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "Çréäté Ⱡ'σяєм ιρѕυ#" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "Çhöösé mödé Ⱡ'σяєм ιρѕυм ∂σłσя #" diff --git a/conf/locale/es_419/LC_MESSAGES/django.mo b/conf/locale/es_419/LC_MESSAGES/django.mo index d433140de6..5fcc54f989 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/django.mo and b/conf/locale/es_419/LC_MESSAGES/django.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/django.po b/conf/locale/es_419/LC_MESSAGES/django.po index 6cd461b8a2..79872b4fcb 100644 --- a/conf/locale/es_419/LC_MESSAGES/django.po +++ b/conf/locale/es_419/LC_MESSAGES/django.po @@ -172,7 +172,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-06-29 17:10+0000\n" "Last-Translator: Cristian Salamea \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/open-edx/edx-platform/language/es_419/)\n" @@ -1413,10 +1413,6 @@ msgstr "Correcto" msgid "incorrect" msgstr "incorrecto" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "parcialmente correcto" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "Incompleto" @@ -1439,10 +1435,6 @@ msgstr "Esto es correcto." msgid "This is incorrect." msgstr "Esto es incorrecto." -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "Esto es parcialmente correcto." - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "Esto no ha sido respondido." @@ -5325,8 +5317,15 @@ msgid "{month} {day}, {year}" msgstr "{day} de {month}, {year}" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." -msgstr "curso ofrecido por {partner_name}, a través de {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." +msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by #. students on this platform @@ -5440,16 +5439,14 @@ msgstr "{platform_name} reconoce el siguiente logro al estudiante" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" -"Este es un certificado válido de {platform_name} para el usuario " -"{user_name}, quién participó en el curso {partner_name} {course_number}" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" -msgstr "Certificado para {partner_name} {course_number} en {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" +msgstr "" #. Translators: This text fragment appears after the student's name #. (displayed in a large font) on the certificate @@ -5616,6 +5613,14 @@ msgstr "" "Si su curso no aparece en el panel de control, contacte a " "{payment_support_link}." +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -6170,6 +6175,23 @@ msgstr "Nombre de usuario {user} ya existe." msgid "File is not attached." msgstr "Archivo no adjuntado." +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "La factura número '{num}' no existe." @@ -6613,6 +6635,10 @@ msgstr "El modo de curso con slug ({mode_slug}) no existe." msgid "CourseMode price updated successfully" msgstr "Se actualizó correctamente el precio para el modo de curso." +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "Los datos de inscripciones ya están disponibles en {dashboard_link}." @@ -6712,18 +6738,6 @@ msgstr "email externo" msgid "Grades for assignment \"{name}\"" msgstr "Calificaciones para la tarea \"{name}\"" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "Se encontraron {num} registros para exportar." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "No se ha encontrado ningún módulo con esa url." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "Estado del estudiante para el problema {problem}" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "Calificaciones de {course_id}" @@ -6903,6 +6917,12 @@ msgstr "borrado" msgid "emailed" msgstr "enviado al correo electrónico" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "generado" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6915,12 +6935,6 @@ msgstr "calificado" msgid "problem distribution graded" msgstr "Distribución de calificaciones del problema" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "generado" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -8607,12 +8621,12 @@ msgid "course_id must be provided" msgstr "Debe suministrar un ID de curso" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" -msgstr "El ID de tema proporcionado {topic_id} no es válido" +msgid "text_search and order_by cannot be provided together" +msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." -msgstr "text_search actualmente no está soportado" +msgid "The supplied topic id {topic_id} is not valid" +msgstr "El ID de tema proporcionado {topic_id} no es válido" #. Translators: 'ordering' is a string describing a way #. of ordering a list. For example, {ordering} may be @@ -10466,6 +10480,10 @@ msgstr "Ayuda" msgid "Sign Out" msgstr "Cerrar sesión" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "Todos los Derechos Reservados" @@ -13181,8 +13199,10 @@ msgid "Section:" msgstr "Sección:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "urlname del problema:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -15244,6 +15264,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "Generar reporte de resultados para examenes supervisados" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -17851,67 +17885,51 @@ msgid "This module is not enabled." msgstr "Este módulo no está habilitado." #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" -"Al completar con éxito el curso, los alumnos reciben un certificado para " -"reconocer su logro. Si usted es un miembro del equipo de curso con el rol de" -" administrador en Studio, puede configurar los certificados del curso." #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" -"Haga clic en {em_start}Agregar su primer certificado{em_end} para agregar " -"una configuración de certificados. Cargue el logotipo de la organización " -"para ser utilizado en el certificado, y especifique al menos un signatario. " -"Puede incluir hasta cuatro firmantes de un certificado. También puede cargar" -" un archivo de imagen de la firma de cada signatario. " -"{em_start}Nota:{em_end} Las imágenes de la firma sólo se utilizan para los " -"certificados verificados. Opcionalmente, especifique un título de curso " -"diferente a utilizar en su certificado. Es posible que desee utilizar un " -"título diferente si, por ejemplo, el nombre oficial del curso es demasiado " -"largo para que aparezca completo en el certificado." #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" -"Seleccione un modo de curso y haga clic en {em_start}Vista previa de " -"Certificado{em_end} para previsualizar el certificado que un estudiante en " -"el modo de curso seleccionado recibiría. Cuando el certificado esté listo " -"para la expedición, haga clic {em_start}Activar{em_end}. Para detener la " -"emisión de un certificado activo, haga clic en {em_start}Desactivar{em_end}." #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" -"Para editar la configuración del certificado, pase el puntero sobre la " -"esquina superior derecha del formulario y haga clic en " -"{em_start}Editar{em_end}. Para eliminar un certificado, pase el puntero " -"sobre la esquina superior derecha del formulario y haga clic en el icono de " -"eliminación. En general, no elimine los certificados después de que un curso" -" haya iniciado, debido a que algunos certificados pueden ya haber sido " -"emitidos a los alumnos." #: cms/templates/certificates.html msgid "Learn more about certificates" diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.mo b/conf/locale/es_419/LC_MESSAGES/djangojs.mo index 36fb2892ff..860e179d44 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/djangojs.mo and b/conf/locale/es_419/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.po b/conf/locale/es_419/LC_MESSAGES/djangojs.po index d80fae9013..556490dcc1 100644 --- a/conf/locale/es_419/LC_MESSAGES/djangojs.po +++ b/conf/locale/es_419/LC_MESSAGES/djangojs.po @@ -25,6 +25,7 @@ # Lalo Cabrera , 2014 # Luis Ricardo Ruiz , 2013 # Luis Ricardo Ruiz , 2013 +# Mecabotware , 2015 # Natalia, 2013 # Natalia, 2013-2014 # Nuri Plans Toral , 2015 @@ -63,6 +64,7 @@ # This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE. # # Translators: +# Claudio Anibal Barahona Flores , 2015 # Cristian Salamea , 2014-2015 # Giancarlo De Agostini , 2015 # Jonathan Amaya , 2014 @@ -70,6 +72,7 @@ # Juan Camilo Montoya Franco , 2014 # Juan Fernando Villa , 2015 # Juan , 2015 +# Mecabotware , 2015 # Nuri Plans Toral , 2014 # Patricia Colmenares , 2015 # paul ochoa , 2015 @@ -89,14 +92,15 @@ # Juan Camilo Montoya Franco , 2014 # Juan , 2015 # karlman72 , 2014 +# Leandro Bohnhoff , 2015 # Patricia Colmenares , 2015 # UAbierta Universidad de Chile , 2015 msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/open-edx/edx-platform/language/es_419/)\n" "MIME-Version: 1.0\n" @@ -142,8 +146,8 @@ msgstr "Aceptar" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -154,6 +158,11 @@ msgstr "Aceptar" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "Cancelar" @@ -163,17 +172,6 @@ msgstr "Cancelar" #: cms/static/js/views/manage_users_and_roles.js #: cms/static/js/views/show_textbook.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/course-outline.underscore -#: cms/templates/js/course_grade_policy.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/group-configuration-editor.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-editor.underscore -#: cms/templates/js/xblock-outline.underscore msgid "Delete" msgstr "Borrar" @@ -248,12 +246,10 @@ msgstr "Error" msgid "Save" msgstr "Guardar" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: cms/static/js/views/modals/edit_xblock.js #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-editor.underscore msgid "Close" msgstr "Cerrar" @@ -346,6 +342,8 @@ msgstr "" #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "Enviar" @@ -780,18 +778,10 @@ msgstr "Propiedades del documento" msgid "Edit HTML" msgstr "Editar HTML" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/course_info_handouts.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/xblock-string-field-editor.underscore msgid "Edit" msgstr "Editar" @@ -1192,11 +1182,9 @@ msgstr "Documento nuevo" msgid "New window" msgstr "Nueva ventana" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/paging-header.underscore msgid "Next" msgstr "Siguiente" @@ -1546,12 +1534,9 @@ msgstr "" "La URL que introdujo parece ser un vínculo externo. ¿Desea agregarle el " "prefijo requerido http://?" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/signatory-editor.underscore msgid "Title" msgstr "Título" @@ -2163,6 +2148,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "¿Está seguro de que desea borrar esta respuesta?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "Mostrando %(first_index)s de un total de %(num_items)s " @@ -2329,6 +2326,7 @@ msgstr "Fecha de publicación" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2408,21 +2406,34 @@ msgid "An unexpected error occurred. Please try again." msgstr "Se produjo un error inesperado. Por favor, inténtelo nuevamente." #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "nombre" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "open_slots" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "Cantidad de equipos" +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "Crear" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "Nombre del equipo (Requerido) *" @@ -2491,20 +2502,48 @@ msgstr "La descripción no puede tener más de 300 caracteres." msgid "You are not currently a member of any team." msgstr "Usted no es actualmente miembro de ningún equipo." +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "Ver %(span_start)s %(team_name)s %(span_end)s" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "Ocurrió un error. Intente nuevamente." -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Confirmar" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "Usted ya pertenece a otro equipo." -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "Este equipo está lleno." @@ -2522,6 +2561,10 @@ msgstr "Todos los equipos" msgid "teams" msgstr "equipos" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "Equipos" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" @@ -2530,33 +2573,52 @@ msgstr "" "Revise los equipos de su curso, organizados por tema. Únase a un equipo para" " colaborar con otros que estén interesados en los mismos temas." -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "Equipos" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "Mi equipo" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "Explorar %(sr_start)s equpos %(sr_end)s" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" -"Cree un nuevo equipo si no puede encontrar uno existente para unirse o si " -"desea aprender con personas que ya conoce." #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "Crear un nuevo equipo" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "Todos los temas" @@ -2579,21 +2641,29 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "%(team_count)s Equipo" msgstr[1] "%(team_count)s Equipos" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "Ver equipos en el tema %(topic_name)s" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" -"Intente {browse_span_start}explorar todos los equipos{span_end} o " -"{search_span_start}buscar descripciones de equipos{span_end}. Sí aún no " -"puede encontrar un equipo para unirse, {create_span_start}cree un nuevo " -"equipo en este tema{span_end}." #: lms/djangoapps/teams/static/teams/js/views/topics.js msgid "All topics" @@ -4082,11 +4152,6 @@ msgstr "Tome una foto de su ID" msgid "Review your info" msgstr "Revise su información" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Confirmar" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "Ocurrió un error. Por favor, intente recargar la página." @@ -4680,7 +4745,7 @@ msgstr "Su archivo ha sido borrado." msgid "Date Added" msgstr "Fecha añadida" -#: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: cms/static/js/views/assets.js msgid "Type" msgstr "Escribir" @@ -5291,19 +5356,6 @@ msgstr "" "La longitud combinada de los campos para la organización y código de la " "librería no puede superar los <%=limit%> caracteres." -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "Campo requerido." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "Por favor, no usar espacios o caracteres especiales en este campo." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" -"Por favor, no utilizar espacios o caracteres especiales en este campo." - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "componente" @@ -5396,11 +5448,526 @@ msgstr "Acciones" msgid "Due Date" msgstr "Fecha límite de entrega" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "Estado" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "gettext(" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "Donar" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "Expandir todo" @@ -5441,12 +6008,6 @@ msgstr "Mover Subsección" msgid "Subsection" msgstr "Subsección" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "gettext(" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5543,10 +6104,6 @@ msgstr "Marcar el examen como completado" msgid "End My Exam" msgstr "Terminar mi examen" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "Donar" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "APRENDER MAS" @@ -6572,6 +7129,16 @@ msgstr "Arrastre y suelte o pulse aquí para subir ficheros de vídeo." msgid "status" msgstr "estado" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "Tipos de problemas comunes" @@ -6646,6 +7213,11 @@ msgstr "ID" msgid "Certificate Details" msgstr "Detalles del certificado" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6692,19 +7264,13 @@ msgstr "" " en los certificados. Dejar vacío para utilizar el título oficial del curso." #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" -msgstr "Añadir signatario" +msgid "Add Additional Signatory" +msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "(Se permiten hasta 4 signatarios por certificado)" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "Crear" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "Elegir modo" @@ -7142,10 +7708,6 @@ msgstr "No ha añadido aún ningún libro de texto a este curso." msgid "Add your first textbook" msgstr "Añada su primer libro de texto" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "Anterior" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "Subidas anteriores" diff --git a/conf/locale/fr/LC_MESSAGES/django.mo b/conf/locale/fr/LC_MESSAGES/django.mo index 410854afc4..8f581c49f4 100644 Binary files a/conf/locale/fr/LC_MESSAGES/django.mo and b/conf/locale/fr/LC_MESSAGES/django.mo differ diff --git a/conf/locale/fr/LC_MESSAGES/django.po b/conf/locale/fr/LC_MESSAGES/django.po index 6962bfb553..73036b11e1 100644 --- a/conf/locale/fr/LC_MESSAGES/django.po +++ b/conf/locale/fr/LC_MESSAGES/django.po @@ -178,7 +178,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-06-19 17:16+0000\n" "Last-Translator: Xavier Antoviaque \n" "Language-Team: French (http://www.transifex.com/open-edx/edx-platform/language/fr/)\n" @@ -340,6 +340,7 @@ msgstr "Cours" msgid "Display Name" msgstr "Nom d'affichage" +#: common/djangoapps/course_modes/models.py #: lms/templates/courseware/course_about.html msgid "Price" msgstr "Prix" @@ -471,7 +472,7 @@ msgstr "Message à afficher lorsque l'utilisateur est bloqué a l'inscription." #: common/djangoapps/embargo/models.py msgid "The message to show when a user is blocked from accessing a course." -msgstr "" +msgstr "Message à afficher lorsqu'un utilisateur a un accès au cours bloqué." #: common/djangoapps/embargo/models.py msgid "" @@ -635,7 +636,7 @@ msgstr "Diplôme de premier cycle supérieur" #: common/djangoapps/student/models.py msgid "Associate degree" -msgstr "" +msgstr "Niveau associé" #: common/djangoapps/student/models.py msgid "Secondary/high school" @@ -658,15 +659,15 @@ msgstr "Aucun" #: common/djangoapps/student/models.py msgid "{platform_name} Honor Code Certificate for {course_name}" -msgstr "" +msgstr "{platform_name} Certificat sur l'honneur pour {course_name}" #: common/djangoapps/student/models.py msgid "{platform_name} Verified Certificate for {course_name}" -msgstr "" +msgstr "{platform_name} Certificat Vérifié pour {course_name}" #: common/djangoapps/student/models.py msgid "{platform_name} Professional Certificate for {course_name}" -msgstr "" +msgstr "{platform_name} Certificat professionnel {course_name}" #: common/djangoapps/student/models.py msgid "" @@ -683,7 +684,7 @@ msgstr "" #: common/djangoapps/student/models.py msgid "{platform_name} Certificate for {course_name}" -msgstr "" +msgstr "{platform_name} Certificat pour {course_name}" #: common/djangoapps/student/models.py msgid "The ISO 639-1 language code for this language." @@ -1371,10 +1372,6 @@ msgstr "correct" msgid "incorrect" msgstr "incorrect" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "incomplet" @@ -1397,10 +1394,6 @@ msgstr "Ceci est correct." msgid "This is incorrect." msgstr "Ceci est incorrect." -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -1455,7 +1448,7 @@ msgstr "" #: common/lib/capa/capa/inputtypes.py msgid "Error running code." -msgstr "" +msgstr "Erreur lors de l'exécution du code." #: common/lib/capa/capa/inputtypes.py msgid "Cannot connect to the queue" @@ -2325,11 +2318,11 @@ msgstr "" #: common/lib/xmodule/xmodule/course_module.py msgid "Date that enrollment for this class is opened" -msgstr "" +msgstr "Date de début des inscriptions pour ce cours" #: common/lib/xmodule/xmodule/course_module.py msgid "Date that enrollment for this class is closed" -msgstr "" +msgstr "Date de fin des inscriptions pour ce cours" #: common/lib/xmodule/xmodule/course_module.py msgid "Start time when this module is visible" @@ -2337,7 +2330,7 @@ msgstr "" #: common/lib/xmodule/xmodule/course_module.py msgid "Date that this class ends" -msgstr "" +msgstr "Date à laquelle se termine ce cours" #: common/lib/xmodule/xmodule/course_module.py msgid "Cosmetic Course Display Price" @@ -3148,7 +3141,7 @@ msgstr "Inscriptions sur invitation par l'équipe pédagogique uniquement." #: common/lib/xmodule/xmodule/course_module.py msgid "Pre-Course Survey Name" -msgstr "Nom de l’Enquête d'Avant Cours " +msgstr "Nom de l’enquête d'avant cours " #: common/lib/xmodule/xmodule/course_module.py msgid "Name of SurveyForm to display as a pre-course survey to the user." @@ -3158,7 +3151,7 @@ msgstr "" #: common/lib/xmodule/xmodule/course_module.py msgid "Pre-Course Survey Required" -msgstr "Enquête d'Avant Cours Requise" +msgstr "Enquête d'avant cours requise" #: common/lib/xmodule/xmodule/course_module.py msgid "" @@ -3574,11 +3567,11 @@ msgstr "" #: common/lib/xmodule/xmodule/library_content_module.py msgid "Invalid Library" -msgstr "" +msgstr "Bibliothèque invalide" #: common/lib/xmodule/xmodule/library_content_module.py msgid "No Library Selected" -msgstr "" +msgstr "Aucune bibliothèque sélectionnée" #: common/lib/xmodule/xmodule/library_root_xblock.py msgid "Enter the name of the library as it should appear in Studio." @@ -3708,7 +3701,7 @@ msgstr "" #: common/lib/xmodule/xmodule/lti_module.py msgid "Request user's username" -msgstr "" +msgstr "Demander le nom de l'utilisateur" #: common/lib/xmodule/xmodule/lti_module.py msgid "" @@ -5088,7 +5081,14 @@ msgid "{month} {day}, {year}" msgstr "" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -5188,13 +5188,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -5351,6 +5351,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5897,6 +5905,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -6296,6 +6321,10 @@ msgstr "CourseMode en mode ralenti ({mode_slug}) n'existe pas" msgid "CourseMode price updated successfully" msgstr "Le prix du ​​CourseMode a été correctement mis à jour" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -6393,18 +6422,6 @@ msgstr "Email externe" msgid "Grades for assignment \"{name}\"" msgstr "Notes pour le travail \"{name}\"" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr " A trouvé {num} enregistrements à jeter." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "Impossible de trouver un module avec cette URL." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "Etat de l’étudiant pour le problème {problem}" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "Notes de {course_id}" @@ -6579,6 +6596,12 @@ msgstr "supprimé" msgid "emailed" msgstr "envoyé par e-mail" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6591,12 +6614,6 @@ msgstr "noté" msgid "problem distribution graded" msgstr "" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -8118,11 +8135,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9805,7 +9822,7 @@ msgstr "Numéro du cours" #: cms/templates/course_outline.html #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Course Start Date:" -msgstr "" +msgstr "Date de début du cours :" #: cms/templates/html_error.html lms/templates/module-error.html msgid "Error:" @@ -9916,7 +9933,7 @@ msgstr "Menu du cours" #: cms/templates/widgets/header.html lms/templates/navigation-edx.html #: lms/templates/navigation.html msgid "Account" -msgstr "" +msgstr "Compte" #: cms/templates/widgets/header.html lms/templates/help_modal.html #: lms/templates/static_templates/help.html wiki/plugins/help/wiki_plugin.py @@ -9928,6 +9945,10 @@ msgstr "Aide" msgid "Sign Out" msgstr "Se déconnecter" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "Tous droits réservés" @@ -12570,8 +12591,10 @@ msgid "Section:" msgstr "Section :" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "url de l'exercice :" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -14379,7 +14402,7 @@ msgstr "Information d'inscription" #. 'audit') #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Number of enrollees (admins, staff, and students) by track" -msgstr "" +msgstr "Nombre d'inscrits (admins, staff, and students) par mode" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Audit" @@ -14403,7 +14426,7 @@ msgstr "Nom d'affichage du cours :" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Course End Date:" -msgstr "" +msgstr "Date de fin du cours" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Has the course started?" @@ -14423,7 +14446,7 @@ msgstr "Le cours est-il terminé ?" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Number of sections:" -msgstr "" +msgstr "Nombre de sections:" #: lms/templates/instructor/instructor_dashboard_2/course_info.html msgid "Grade Cutoffs:" @@ -14480,6 +14503,11 @@ msgid "" "background, meaning it is OK to navigate away from this page while your " "report is generating." msgstr "" +"Pour les cours à grande échelle, la création des rapports peut prendre " +"plusieurs heures. Quand la création du rapport est achevée, un lien incluant" +" la date et l'heure de la création est affiché dans le tableau ci-dessous. " +"Ces rapports sont créés en tâche de fond, cela signifie qu'il est possible " +"de naviguer dans d'autres pages en parallèle de la création des rapports." #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" @@ -14519,6 +14547,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -15218,13 +15260,15 @@ msgstr "Importer CSV" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Batch Beta Tester Addition" -msgstr "Ajout par des bêta testeurs par lots" +msgstr "Ajout de bêta testeurs" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "" "Note: Users must have an activated {platform_name} account before they can " "be enrolled as beta testers." msgstr "" +"Note: Les utilisateurs doivent avoir activé leur compte {platform_name} " +"avant de pouvoir être ajouté en bêta testeur." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "" @@ -15252,14 +15296,14 @@ msgstr "Supprimer des bêta-testeurs" #. users can be added to. #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Course Team Management" -msgstr "" +msgstr "Gestion de l'équipe du cours" #. Translators: an "Administrator Group" is a group, such as Course Staff, #. that #. users can be added to. #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Select a course team role:" -msgstr "" +msgstr "Sélectionner un rôle:" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Getting available lists..." @@ -15279,10 +15323,16 @@ msgid "" "all course data. Staff also have access to your course in Studio and " "Insights. You can only give course team roles to enrolled users." msgstr "" +"Les membres de l'équipe avec le rôle Équipe pédagogique aident à la gestion " +"du cours. L'équipe pédagogique peut inscrire et désinscrire des " +"participants, ainsi que modifier les notes aux exercices et accéder à " +"l'ensemble des données du cours. L'équipe pédagogique a également accès au " +"cours dans Studio. Vous ne pouvez affecter des rôles que pour les " +"utilisateurs inscrits au cours." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Staff" -msgstr "Ajouter à l‘équipe" +msgstr "Ajouter à Équipe pédagogique" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "" @@ -15292,10 +15342,15 @@ msgid "" " to manage course team membership. You can only give course team roles to " "enrolled users." msgstr "" +"Les membres de l'équipe avec le rôle Admin aident à la gestion du cours. Ils" +" peuvent également effectuer toutes les actions comme l'Équipe pédagogique, " +"mais aussi ajouter ou supprimer des membres Admin, gérer les rôles de " +"modérations des forums, l'ajout de bêta-testeurs. Vous ne pouvez affecter " +"des rôles que pour les utilisateurs inscrits au cours." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Admin" -msgstr "" +msgstr "Ajouter un Admin" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Beta Testers" @@ -15307,6 +15362,10 @@ msgid "" "sure that the content works, but have no additional privileges. You can only" " give course team roles to enrolled users." msgstr "" +"Les Bêta-testeurs peuvent voir le contenu du cours avant les autres " +"participants. Ils peuvent ainsi s'assurer que le contenu fonctionne, mais ne" +" disposent pas de permissions supplémentaires. Vous ne pouvez affecter de " +"rôle uniquement à des utilisateurs inscrits." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Beta Tester" @@ -15324,6 +15383,12 @@ msgid "" "moderation roles to manage course team membership. You can only give course " "team roles to enrolled users." msgstr "" +"Les Administrateurs de discussions peuvent modifier et supprimer tous les " +"post, les dénonciations abusives, fermer et réouvrir des sujets, indiquer " +"des réponses et voir les posts de toutes les cohortes. Leur posts sont " +"marqués comme 'Équipe pédagogique'. Ils peuvent aussi modifier les rôles de " +"modérations des utilisateurs. Vous pouvez donner un rôle d'équipe uniquement" +" à des utilisateurs inscrits au cours." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Discussion Admin" @@ -15341,6 +15406,12 @@ msgid "" " by adding or removing discussion moderation roles. You can only give course" " team roles to enrolled users." msgstr "" +"Les Modérateurs de discussions peuvent modifier et supprimer tous les post, " +"les dénonciations abusives, fermer et réouvrir des sujets, indiquer des " +"réponses et voir les posts de toutes les cohortes. Leur posts sont marqués " +"comme 'Équipe pédagogique'. Ils ne peuvent pas modifier les rôles de " +"modérations des utilisateurs. Vous pouvez donner un rôle d'équipe uniquement" +" à des utilisateurs inscrits au cours." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Moderator" @@ -15358,10 +15429,16 @@ msgid "" "from all cohorts. Their posts are marked as 'Community TA'. You can only " "give course team roles to enrolled users." msgstr "" +"Les Assistants peuvent modifier et supprimer tous les post, les " +"dénonciations abusives, fermer et réouvrir des sujets, indiquer des réponses" +" et voir les posts de toutes les cohortes. Leur posts sont marqués comme " +"'Assistants'. Ils ne peuvent pas modifier les rôles de modérations des " +"utilisateurs. Vous pouvez donner un rôle d'équipe uniquement à des " +"utilisateurs inscrits au cours." #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Community TA" -msgstr "Ajouter un Community TA" +msgstr "Ajouter un Assistant" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "CCX Coaches" @@ -16568,7 +16645,7 @@ msgstr "Se connecter ou s'inscrire" #: lms/templates/student_profile/learner_profile.html msgid "Learner Profile" -msgstr "" +msgstr "Profil de participant" #. Translators: this section lists all the third-party authentication #. providers @@ -16614,7 +16691,7 @@ msgstr "Enquête Utilisateur" #: lms/templates/survey/survey.html msgid "Pre-Course Survey" -msgstr "" +msgstr "Enquête d'avant-cours " #: lms/templates/survey/survey.html msgid "" @@ -16623,6 +16700,9 @@ msgid "" " use of {platform_name} only. It will not be linked to your public profile " "in any way." msgstr "" +"Vous pourrez démarrer votre cours une fois rempli le formulaire suivant. Les" +" champs obligatoires sont signalés par une astérisque (*). Ces informations " +"sont utilisées uniquement par {platform_name}." #: lms/templates/survey/survey.html msgid "You are missing the following required fields:" @@ -16669,7 +16749,7 @@ msgstr "" #: lms/templates/verify_student/incourse_reverify.html msgid "Re-Verify for {course_name}" -msgstr "" +msgstr "Re-Vérification pour {course_name}" #: lms/templates/verify_student/missed_deadline.html msgid "Verification Deadline Has Passed" @@ -16984,41 +17064,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html @@ -17494,6 +17583,9 @@ msgid "" "date. When you configure a subsection, you can also set the grading policy " "and due date." msgstr "" +"Sélectionnez l'icône Configuration pour une section ou une sous-section pour" +" définir sa date de publication. Lorsque vous configurez une sous-section, " +"vous pouvez également définir le type de devoir et la date d'échéance." #: cms/templates/course_outline.html msgid "Changing the content students see" diff --git a/conf/locale/fr/LC_MESSAGES/djangojs.mo b/conf/locale/fr/LC_MESSAGES/djangojs.mo index 4a7947aedb..f755a7a260 100644 Binary files a/conf/locale/fr/LC_MESSAGES/djangojs.mo and b/conf/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/fr/LC_MESSAGES/djangojs.po b/conf/locale/fr/LC_MESSAGES/djangojs.po index 9febd93f54..20432d7a44 100644 --- a/conf/locale/fr/LC_MESSAGES/djangojs.po +++ b/conf/locale/fr/LC_MESSAGES/djangojs.po @@ -110,8 +110,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: French (http://www.transifex.com/open-edx/edx-platform/language/fr/)\n" "MIME-Version: 1.0\n" @@ -157,8 +157,8 @@ msgstr "OK" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -169,6 +169,11 @@ msgstr "OK" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "Annuler" @@ -178,17 +183,6 @@ msgstr "Annuler" #: cms/static/js/views/manage_users_and_roles.js #: cms/static/js/views/show_textbook.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/course-outline.underscore -#: cms/templates/js/course_grade_policy.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/group-configuration-editor.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-editor.underscore -#: cms/templates/js/xblock-outline.underscore msgid "Delete" msgstr "Supprimer" @@ -263,12 +257,10 @@ msgstr "Erreur" msgid "Save" msgstr "Enregistrer" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: cms/static/js/views/modals/edit_xblock.js #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-editor.underscore msgid "Close" msgstr "Fermer" @@ -358,6 +350,8 @@ msgstr "Votre résultat n'est pas suffisant pour passer à l'étape suivante." #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "Soumettre" @@ -790,18 +784,10 @@ msgstr "Propriétés du document" msgid "Edit HTML" msgstr "Editer le code HTML" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/course_info_handouts.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/xblock-string-field-editor.underscore msgid "Edit" msgstr "Éditer" @@ -1555,12 +1541,9 @@ msgstr "" "L'URL que vous avez entrée semble être un lien externe. Voulez-vous ajouter " "le préfixe http:// requis ?" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/signatory-editor.underscore msgid "Title" msgstr "Titre" @@ -2187,6 +2170,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "Voulez-vous vraiment supprimer cette réponse ?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "%(first_index)s sur %(num_items)s au total" @@ -2352,6 +2347,7 @@ msgstr "Date de l'envoi" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2431,21 +2427,34 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "nom" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "total équipe" +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "Créer" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2470,6 +2479,7 @@ msgid "Language" msgstr "Langue" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2480,6 +2490,7 @@ msgid "Country" msgstr "Pays" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2512,20 +2523,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "Confirmer" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2543,37 +2582,62 @@ msgstr "" msgid "teams" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "Équipes" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" " with other learners who are interested in the same topic as you are." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "Équipes" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2596,15 +2660,27 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "%(team_count)s Equipe" msgstr[1] "%(team_count)s Équipes" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "Voir les équipes du sujet %(topic_name)s" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -4073,11 +4149,6 @@ msgstr "Prenez une photo de votre pièce d'identité" msgid "Review your info" msgstr "Vérifiez vos informations" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "Confirmer" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "Une erreur est survenue. Essayez de rafraîchir la page." @@ -4591,6 +4662,7 @@ msgstr "" #: cms/static/js/models/settings/course_details.js msgid "The course end date must be later than the course start date." msgstr "" +"La date de fin du cours doit être postérieure à la date de début du cours." #: cms/static/js/models/settings/course_details.js msgid "The course start date must be later than the enrollment start date." @@ -4669,7 +4741,7 @@ msgstr "Votre fichier a été supprimé." msgid "Date Added" msgstr "Date ajoutée" -#: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: cms/static/js/views/assets.js msgid "Type" msgstr "Type" @@ -5252,19 +5324,6 @@ msgstr "" "La longueur totale des champs organisation et codes de bibliothèque ne doit " "pas dépasser <%=limit%> caractères." -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "Champ requis." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "Merci de ne pas utiliser d'espace dans ce champ." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" -"Merci de ne pas utiliser d'espace ou de caractère spécial dans ce champ." - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "composant" @@ -5356,11 +5415,526 @@ msgstr "Actions" msgid "Due Date" msgstr "Date d'échéance" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "Statut" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "gettext(" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "Faire un don" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5401,12 +5975,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "gettext(" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5503,10 +6071,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "Faire un don" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "EN SAVOIR PLUS" @@ -6483,6 +7047,16 @@ msgstr "Glisser déposer ou cliquer ici pour importer des fichiers vidéo." msgid "status" msgstr "statut" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "Types d'exercices classiques" @@ -6557,6 +7131,11 @@ msgstr "Identifiant" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6601,19 +7180,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "Créer" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -7055,10 +7628,6 @@ msgstr "Vous n'avez encore ajouté aucun manuel à ce cours." msgid "Add your first textbook" msgstr "Ajouter votre premier manuel" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/he/LC_MESSAGES/django.mo b/conf/locale/he/LC_MESSAGES/django.mo index d3279a7da9..69147e6056 100644 Binary files a/conf/locale/he/LC_MESSAGES/django.mo and b/conf/locale/he/LC_MESSAGES/django.mo differ diff --git a/conf/locale/he/LC_MESSAGES/django.po b/conf/locale/he/LC_MESSAGES/django.po index c4747d6637..bfb133a9ef 100644 --- a/conf/locale/he/LC_MESSAGES/django.po +++ b/conf/locale/he/LC_MESSAGES/django.po @@ -61,7 +61,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-05-28 20:00+0000\n" "Last-Translator: Nadav Stark \n" "Language-Team: Hebrew (http://www.transifex.com/open-edx/edx-platform/language/he/)\n" @@ -1215,10 +1215,6 @@ msgstr "" msgid "incorrect" msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "" @@ -1241,10 +1237,6 @@ msgstr "" msgid "This is incorrect." msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -4550,7 +4542,14 @@ msgid "{month} {day}, {year}" msgstr "" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -4650,13 +4649,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -4813,6 +4812,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5333,6 +5340,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -5719,6 +5743,10 @@ msgstr "" msgid "CourseMode price updated successfully" msgstr "" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -5816,18 +5844,6 @@ msgstr "" msgid "Grades for assignment \"{name}\"" msgstr "" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "" @@ -5991,6 +6007,12 @@ msgstr "" msgid "emailed" msgstr "" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6003,12 +6025,6 @@ msgstr "" msgid "problem distribution graded" msgstr "" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7392,6 +7408,7 @@ msgid "Optional language the team uses as ISO 639-1 code." msgstr "" #: lms/djangoapps/teams/plugins.py +#: lms/djangoapps/teams/templates/teams/teams.html msgid "Teams" msgstr "" @@ -7404,11 +7421,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9112,6 +9129,10 @@ msgstr "" msgid "Sign Out" msgstr "" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -11605,7 +11626,9 @@ msgid "Section:" msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html @@ -13377,6 +13400,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -15674,41 +15711,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html diff --git a/conf/locale/he/LC_MESSAGES/djangojs.mo b/conf/locale/he/LC_MESSAGES/djangojs.mo index b535c98d18..87a15378ee 100644 Binary files a/conf/locale/he/LC_MESSAGES/djangojs.mo and b/conf/locale/he/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/he/LC_MESSAGES/djangojs.po b/conf/locale/he/LC_MESSAGES/djangojs.po index ff427cfb05..8da793ef73 100644 --- a/conf/locale/he/LC_MESSAGES/djangojs.po +++ b/conf/locale/he/LC_MESSAGES/djangojs.po @@ -44,8 +44,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Hebrew (http://www.transifex.com/open-edx/edx-platform/language/he/)\n" "MIME-Version: 1.0\n" @@ -91,8 +91,8 @@ msgstr "" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -103,6 +103,11 @@ msgstr "" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "" @@ -123,6 +128,7 @@ msgstr "" #: cms/templates/js/show-textbook.underscore #: cms/templates/js/signatory-editor.underscore #: cms/templates/js/xblock-outline.underscore +#: common/static/common/templates/discussion/forum-action-delete.underscore msgid "Delete" msgstr "" @@ -203,6 +209,8 @@ msgstr "" #: cms/static/js/views/modals/edit_xblock.js #: common/lib/xmodule/xmodule/js/src/html/edit.js #: cms/templates/js/signatory-editor.underscore +#: common/static/common/templates/image-modal.underscore +#: common/static/common/templates/discussion/forum-action-close.underscore msgid "Close" msgstr "" @@ -292,6 +300,8 @@ msgstr "" #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "" @@ -726,6 +736,7 @@ msgstr "" #: cms/templates/js/show-textbook.underscore #: cms/templates/js/signatory-details.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/forum-action-edit.underscore msgid "Edit" msgstr "" @@ -813,9 +824,11 @@ msgstr "" msgid "Formats" msgstr "" +#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/templates/image-modal.underscore msgid "Fullscreen" msgstr "" @@ -1131,6 +1144,8 @@ msgstr "" #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore msgid "Next" msgstr "" @@ -1840,6 +1855,7 @@ msgstr "" #: common/static/coffee/src/discussion/utils.js #: common/static/coffee/src/discussion/views/discussion_thread_list_view.js #: common/static/coffee/src/discussion/views/discussion_topic_menu_view.js +#: common/static/common/templates/discussion/pagination.underscore msgid "…" msgstr "" @@ -2014,6 +2030,8 @@ msgid "Your post will be discarded." msgstr "" #: common/static/coffee/src/discussion/views/response_comment_show_view.js +#: common/static/common/templates/discussion/post-user-display.underscore +#: common/static/common/templates/discussion/profile-thread.underscore msgid "anonymous" msgstr "" @@ -2029,6 +2047,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "" @@ -2194,6 +2224,7 @@ msgstr "" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2212,6 +2243,8 @@ msgid "Public" msgstr "" #: common/static/js/vendor/ova/catch/js/catch.js +#: common/static/common/templates/components/search-field.underscore +#: lms/djangoapps/support/static/support/templates/certificates.underscore msgid "Search" msgstr "" @@ -2273,13 +2306,16 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -#: lms/djangoapps/teams/static/teams/js/collections/topic.js -#: lms/templates/edxnotes/tab-item.underscore -msgid "name" +msgid "last activity" msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" +msgid "open slots" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/topic.js +#: lms/templates/edxnotes/tab-item.underscore +msgid "name" msgstr "" #. Translators: This refers to the number of teams (a count of how many teams @@ -2288,6 +2324,17 @@ msgstr "" msgid "team count" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2312,6 +2359,7 @@ msgid "Language" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2322,6 +2370,7 @@ msgid "Country" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2354,20 +2403,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2386,13 +2463,13 @@ msgid "teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"See all teams in your course, organized by topic. Join a team to collaborate" -" with other learners who are interested in the same topic as you are." +msgid "Teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" +msgid "" +"See all teams in your course, organized by topic. Join a team to collaborate" +" with other learners who are interested in the same topic as you are." msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -2400,22 +2477,47 @@ msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2438,15 +2540,27 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "" msgstr[1] "" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -3784,11 +3898,6 @@ msgstr "" msgid "Review your info" msgstr "" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -4151,6 +4260,7 @@ msgstr "" #: cms/static/js/factories/manage_users.js #: cms/static/js/factories/manage_users_lib.js +#: common/static/common/templates/discussion/post-user-display.underscore msgid "Staff" msgstr "" @@ -4334,6 +4444,7 @@ msgid "Date Added" msgstr "" #: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore msgid "Type" msgstr "" @@ -4890,18 +5001,6 @@ msgid "" "more than <%=limit%> characters." msgstr "" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "" @@ -4991,11 +5090,526 @@ msgstr "" msgid "Due Date" msgstr "" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5036,12 +5650,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5131,10 +5739,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "" @@ -6051,6 +6655,16 @@ msgstr "" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "" @@ -6125,6 +6739,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6169,19 +6788,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6606,10 +7219,6 @@ msgstr "" msgid "Add your first textbook" msgstr "" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/hi/LC_MESSAGES/django.mo b/conf/locale/hi/LC_MESSAGES/django.mo index 1aba656f1b..fe3f219e98 100644 Binary files a/conf/locale/hi/LC_MESSAGES/django.mo and b/conf/locale/hi/LC_MESSAGES/django.mo differ diff --git a/conf/locale/hi/LC_MESSAGES/django.po b/conf/locale/hi/LC_MESSAGES/django.po index ee7e0c414b..4aae8e59a1 100644 --- a/conf/locale/hi/LC_MESSAGES/django.po +++ b/conf/locale/hi/LC_MESSAGES/django.po @@ -72,7 +72,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-06-28 20:21+0000\n" "Last-Translator: ria1234 \n" "Language-Team: Hindi (http://www.transifex.com/open-edx/edx-platform/language/hi/)\n" @@ -1227,10 +1227,6 @@ msgstr "" msgid "incorrect" msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "" @@ -1253,10 +1249,6 @@ msgstr "" msgid "This is incorrect." msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -4587,7 +4579,14 @@ msgid "{month} {day}, {year}" msgstr "" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -4687,13 +4686,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -4849,6 +4848,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5384,6 +5391,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -5770,6 +5794,10 @@ msgstr "" msgid "CourseMode price updated successfully" msgstr "" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -5865,18 +5893,6 @@ msgstr "" msgid "Grades for assignment \"{name}\"" msgstr "" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "" @@ -6040,6 +6056,12 @@ msgstr "नष्ट कर दिया गया" msgid "emailed" msgstr "ईमेल कर दी गई" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6052,12 +6074,6 @@ msgstr "श्रेणी दी जा चुकी है" msgid "problem distribution graded" msgstr "" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7517,6 +7533,7 @@ msgid "Optional language the team uses as ISO 639-1 code." msgstr "" #: lms/djangoapps/teams/plugins.py +#: lms/djangoapps/teams/templates/teams/teams.html msgid "Teams" msgstr "" @@ -7529,11 +7546,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9269,6 +9286,10 @@ msgstr "सहायता" msgid "Sign Out" msgstr "" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -11836,8 +11857,10 @@ msgid "Section:" msgstr "धारा:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "समस्या का यू आर एल दें:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -13650,6 +13673,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -16013,41 +16050,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html diff --git a/conf/locale/hi/LC_MESSAGES/djangojs.mo b/conf/locale/hi/LC_MESSAGES/djangojs.mo index adc0b21aaf..77296b2397 100644 Binary files a/conf/locale/hi/LC_MESSAGES/djangojs.mo and b/conf/locale/hi/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/hi/LC_MESSAGES/djangojs.po b/conf/locale/hi/LC_MESSAGES/djangojs.po index a575ccc3e3..f91d889c2f 100644 --- a/conf/locale/hi/LC_MESSAGES/djangojs.po +++ b/conf/locale/hi/LC_MESSAGES/djangojs.po @@ -46,8 +46,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Hindi (http://www.transifex.com/open-edx/edx-platform/language/hi/)\n" "MIME-Version: 1.0\n" @@ -93,8 +93,8 @@ msgstr "ठीक" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -105,6 +105,11 @@ msgstr "ठीक" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "रद्द करें" @@ -125,6 +130,7 @@ msgstr "रद्द करें" #: cms/templates/js/show-textbook.underscore #: cms/templates/js/signatory-editor.underscore #: cms/templates/js/xblock-outline.underscore +#: common/static/common/templates/discussion/forum-action-delete.underscore msgid "Delete" msgstr "" @@ -736,6 +742,7 @@ msgstr "" #: cms/templates/js/show-textbook.underscore #: cms/templates/js/signatory-details.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/forum-action-edit.underscore msgid "Edit" msgstr "" @@ -823,9 +830,11 @@ msgstr "" msgid "Formats" msgstr "" +#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/templates/image-modal.underscore msgid "Fullscreen" msgstr "" @@ -1141,6 +1150,8 @@ msgstr "" #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore msgid "Next" msgstr "" @@ -1486,6 +1497,8 @@ msgstr "" #: common/lib/xmodule/xmodule/js/src/html/edit.js #: cms/templates/js/signatory-details.underscore #: cms/templates/js/signatory-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/thread-edit.underscore msgid "Title" msgstr "" @@ -2048,6 +2061,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "क्या आप वाकई इस जवाब को हटाना चाहते हैं?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "" @@ -2213,6 +2238,7 @@ msgstr "" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2231,6 +2257,8 @@ msgid "Public" msgstr "" #: common/static/js/vendor/ova/catch/js/catch.js +#: common/static/common/templates/components/search-field.underscore +#: lms/djangoapps/support/static/support/templates/certificates.underscore msgid "Search" msgstr "" @@ -2292,13 +2320,16 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -#: lms/djangoapps/teams/static/teams/js/collections/topic.js -#: lms/templates/edxnotes/tab-item.underscore -msgid "name" +msgid "last activity" msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" +msgid "open slots" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/topic.js +#: lms/templates/edxnotes/tab-item.underscore +msgid "name" msgstr "" #. Translators: This refers to the number of teams (a count of how many teams @@ -2307,6 +2338,17 @@ msgstr "" msgid "team count" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2331,6 +2373,7 @@ msgid "Language" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2341,6 +2384,7 @@ msgid "Country" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2373,20 +2417,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2405,13 +2477,13 @@ msgid "teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"See all teams in your course, organized by topic. Join a team to collaborate" -" with other learners who are interested in the same topic as you are." +msgid "Teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" +msgid "" +"See all teams in your course, organized by topic. Join a team to collaborate" +" with other learners who are interested in the same topic as you are." msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -2419,22 +2491,47 @@ msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2457,15 +2554,27 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "" msgstr[1] "" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -3832,11 +3941,6 @@ msgstr "" msgid "Review your info" msgstr "" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -4394,6 +4498,7 @@ msgid "Date Added" msgstr "जोड़ी गयी तिथि" #: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore msgid "Type" msgstr "" @@ -4954,18 +5059,6 @@ msgid "" "more than <%=limit%> characters." msgstr "" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "आवश्यक क्षेत्र |" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "इस क्षेत्र में किसी भी रिक्त स्थान का प्रयोग न करें| " - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "कृपया इस क्षेत्र में खाली स्थान या विशेष अक्षरों का प्रयोग न करें।" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "" @@ -5057,11 +5150,526 @@ msgstr "" msgid "Due Date" msgstr "" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5102,12 +5710,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5197,10 +5799,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "" @@ -6117,6 +6715,16 @@ msgstr "" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "" @@ -6191,6 +6799,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6235,19 +6848,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6672,10 +7279,6 @@ msgstr "" msgid "Add your first textbook" msgstr "" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/ko_KR/LC_MESSAGES/django.mo b/conf/locale/ko_KR/LC_MESSAGES/django.mo index 97749a9ff2..b85a66fabe 100644 Binary files a/conf/locale/ko_KR/LC_MESSAGES/django.mo and b/conf/locale/ko_KR/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ko_KR/LC_MESSAGES/django.po b/conf/locale/ko_KR/LC_MESSAGES/django.po index 6ee070db15..7a81c5705a 100644 --- a/conf/locale/ko_KR/LC_MESSAGES/django.po +++ b/conf/locale/ko_KR/LC_MESSAGES/django.po @@ -87,7 +87,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-08-14 05:59+0000\n" "Last-Translator: Hongseob Lee \n" "Language-Team: Korean (Korea) (http://www.transifex.com/open-edx/edx-platform/language/ko_KR/)\n" @@ -248,7 +248,7 @@ msgstr "강좌" #: common/lib/xmodule/xmodule/word_cloud_module.py #: cms/templates/container.html cms/templates/library.html msgid "Display Name" -msgstr "표시 이름" +msgstr "메뉴명" #: common/djangoapps/course_modes/models.py #: lms/templates/courseware/course_about.html @@ -279,7 +279,7 @@ msgstr "" #: common/djangoapps/course_modes/models.py msgid "Honor Code Certificate" -msgstr "명예 과정 수료증" +msgstr "명예 과정 이수증" #: common/djangoapps/course_modes/models.py msgid "" @@ -321,7 +321,7 @@ msgstr "명예 과정의 학습자로 등록되었습니다." #: openedx/core/djangoapps/user_api/views.py #: lms/templates/static_templates/honor.html msgid "Honor Code" -msgstr "명예 규범" +msgstr "학습자 서약" #: common/djangoapps/course_modes/models.py msgid "You're auditing this course" @@ -497,7 +497,7 @@ msgstr "국가를 입력하세요." #: common/djangoapps/student/forms.py msgid "To enroll, you must follow the honor code." -msgstr "K-MOOC를 이용하려면, 명예규범을 준수해야 합니다." +msgstr "K-MOOC를 이용하려면, 학습자 서약을 준수해야 합니다." #: common/djangoapps/student/forms.py msgid "You are missing one or more required fields" @@ -505,7 +505,7 @@ msgstr "한 개 이상의 필수 항목을 입력하지 않았습니다. " #: common/djangoapps/student/forms.py msgid "Username and password fields cannot match" -msgstr "아이디와 패스워드가 일치하지 않습니다." +msgstr "아이디와 패스워드가 일치합니다. 다르게 입력해주세요." #: common/djangoapps/student/forms.py common/djangoapps/student/views.py msgid "Password: " @@ -1256,10 +1256,6 @@ msgstr "맞음" msgid "incorrect" msgstr "틀림" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "미완성" @@ -1282,10 +1278,6 @@ msgstr "맞습니다." msgid "This is incorrect." msgstr "틀립니다." -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "미응답 상태입니다." @@ -2492,7 +2484,7 @@ msgstr "다른 이용 가능한 강좌 저작 도구를 찾아서, 필요할 때 #: common/lib/xmodule/xmodule/course_module.py msgid "Draft a Rough Course Outline" -msgstr "강좌 개요 초안" +msgstr "강좌 개요 초안 만들기" #: common/lib/xmodule/xmodule/course_module.py msgid "Create Your First Section and Subsection" @@ -2607,7 +2599,7 @@ msgstr "도움말 다운로드" #: common/lib/xmodule/xmodule/course_module.py msgid "Draft Your Course About Page" -msgstr "강좌 안내 작성" +msgstr "강좌 안내 작성하기" #: common/lib/xmodule/xmodule/course_module.py msgid "Draft a Course Description" @@ -4744,8 +4736,15 @@ msgid "{month} {day}, {year}" msgstr "{month} {day}, {year}" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." -msgstr " {platform_name}를 통해 {partner_name}에서 제공하는 강좌입니다. " +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." +msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by #. students on this platform @@ -4854,16 +4853,14 @@ msgstr "{platform_name}은 다음 학습자의 성과를 인정합니다. " #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" -"이것은 {partner_name} {course_number}에 참여한 {user_name}님의 유효한 {platform_name} " -"수료증입니다. " #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" -msgstr "{partner_name} {course_number} 수료증 | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" +msgstr "" #. Translators: This text fragment appears after the student's name #. (displayed in a large font) on the certificate @@ -5021,6 +5018,14 @@ msgid "" "{payment_support_link}." msgstr "신청 강좌가 학습자의 대시보드에 나타나지 않을 경우, {payment_support_link} 로 연락바랍니다." +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5403,7 +5408,7 @@ msgstr "수강 등록한 학습자수 " #: lms/djangoapps/dashboard/sysadmin.py msgid "# staff" -msgstr "관리자 인원수 " +msgstr "운영팀 인원수 " #: lms/djangoapps/dashboard/sysadmin.py msgid "instructors" @@ -5474,13 +5479,13 @@ msgstr "{platform_name} 관리자" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py msgid "Course Staff" -msgstr "강좌 관리자" +msgstr "강좌 운영팀" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py #: lms/templates/courseware/course_navigation.html #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Staff" -msgstr "관리자" +msgstr "운영팀" #: lms/djangoapps/instructor/paidcourse_enrollment_report.py msgid "Used Registration Code" @@ -5548,6 +5553,23 @@ msgstr " 아이디 {user} 가 이미 존재합니다. " msgid "File is not attached." msgstr "파일이 첨부되지 않았습니다 " +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "청구서 번호 '{num}' 가 존재하지 않습니다. " @@ -5952,6 +5974,10 @@ msgstr "mode slug({mode_slug}) 강좌 모드는 존재하지 않습니다." msgid "CourseMode price updated successfully" msgstr "CourseMode의 가격이 성공적으로 업데이트되었습니다. " +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "수강등록 데이터를 {dashboard_link}에서 볼 수 있습니다. " @@ -5976,7 +6002,7 @@ msgstr "확장" #: lms/djangoapps/instructor/views/instructor_dashboard.py #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "Data Download" -msgstr "자료 다운받기" +msgstr "데이터" #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "For analytics about your course, go to {analytics_dashboard_name}." @@ -6047,18 +6073,6 @@ msgstr "외부 이메일" msgid "Grades for assignment \"{name}\"" msgstr "\"{name}\" 과제의 성적" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "Found {num} records to dump. " - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "해당 urlname을 가진 모듈을 찾을 수 없습니다. " - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "{problem} 문제 학습자 상태" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr " {course_id} 강좌 성적" @@ -6092,7 +6106,7 @@ msgstr "{action}에 대한 원격 성적기록부 응답" #: lms/djangoapps/instructor/views/legacy.py #: openedx/core/djangoapps/user_api/views.py msgid "Full name" -msgstr "이름" +msgstr "실명" #: lms/djangoapps/instructor/views/legacy.py msgid "{title} in course {course_key}" @@ -6109,7 +6123,7 @@ msgstr "아이디" #: lms/templates/signup_modal.html lms/templates/sysadmin_dashboard.html #: lms/templates/verify_student/face_upload.html msgid "Full Name" -msgstr "이름" +msgstr "실명" #: lms/djangoapps/instructor/views/legacy.py msgid "edX email" @@ -6222,6 +6236,12 @@ msgstr "삭제되었습니다." msgid "emailed" msgstr "이메일이 전송되었습니다." +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "생성되었습니다." + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6234,12 +6254,6 @@ msgstr "채점되었습니다." msgid "problem distribution graded" msgstr "채점된 문제 분포" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "생성되었습니다." - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6643,7 +6657,7 @@ msgstr "제출이 검토를 위해 표시되었습니다." #: lms/djangoapps/open_ended_grading/views.py #: lms/templates/instructor/staff_grading.html msgid "Staff grading" -msgstr "관리자 채점" +msgstr "운영팀 채점" #. Translators: "Peer grading" appears on a tab that allows #. students to view open-ended problems that require grading @@ -7786,12 +7800,12 @@ msgid "course_id must be provided" msgstr "course_id가 제공되어야 합니다." #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" -msgstr "제공된 주제 ID {topic_id}가 유효하지 않습니다." +msgid "text_search and order_by cannot be provided together" +msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." -msgstr "text_search는 제공되지 않습니다." +msgid "The supplied topic id {topic_id} is not valid" +msgstr "제공된 주제 ID {topic_id}가 유효하지 않습니다." #. Translators: 'ordering' is a string describing a way #. of ordering a list. For example, {ordering} may be @@ -8686,7 +8700,7 @@ msgstr "이 글에는 첨부가 없습니다." #: openedx/core/djangoapps/course_groups/cohorts.py msgid "You cannot create two cohorts with the same name" -msgstr "두 학습 집단을 같은 이름으로 만들 수 없습니다." +msgstr "학습집단명은 같을 수 없습니다." #: openedx/core/djangoapps/course_groups/cohorts.py msgid "" @@ -8902,7 +8916,7 @@ msgstr "국가를 선택하십시오" #. in order to register a new account. #: openedx/core/djangoapps/user_api/views.py msgid "Terms of Service and Honor Code" -msgstr "서비스 조항 및 명예 규범" +msgstr "서비스 조항 및 학습자 서약" #: openedx/core/djangoapps/user_api/views.py msgid "I agree to the {platform_name} {terms_of_service}." @@ -9089,7 +9103,7 @@ msgstr "" #: cms/djangoapps/contentstore/views/course.py msgid "Unscheduled" -msgstr "" +msgstr "지정되지 않음" #: cms/djangoapps/contentstore/views/course.py msgid "" @@ -9504,7 +9518,7 @@ msgstr "계정이 이미 활성화되어 있습니다." #: cms/templates/registration/activation_complete.html #: lms/templates/registration/activation_complete.html msgid "Visit your {link_start}dashboard{link_end} to see your courses." -msgstr "강좌를 보기 위해서는 {link_start}대시보드{link_end}를 방문하세요." +msgstr "강좌를 보기 위해 {link_start}대시보드{link_end}로 이동하세요. " #: cms/templates/widgets/header.html lms/templates/courseware/courseware.html msgid "Course Navigation" @@ -9525,6 +9539,10 @@ msgstr "도움말" msgid "Sign Out" msgstr "로그아웃" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "모든 저작권이 보호됩니다." @@ -9567,7 +9585,7 @@ msgstr "축하합니다. {course_name}에 수강 신청이 되었습니다" #: common/templates/course_modes/choose.html msgid "Pursue Academic Credit with a Verified Certificate" -msgstr "인증된 강좌 수료증으로 학점 받기" +msgstr "인증된 이수증으로 학점 받기" #: common/templates/course_modes/choose.html msgid "" @@ -9576,12 +9594,12 @@ msgid "" "qualify for academic credit from {org}, advance your career, or strengthen " "your school applications." msgstr "" -"{org} 기관의 학점을 이수할 수 있을 만큼인 귀하의 새로운 기술과 지식을, 이 인증 수료증으로 보여주세요. 이 수료증을 귀하의 커리어" -" 개발과 학교 지원에 활용하면 도움이 될 것입니다." +"{org} 기관의 학점을 이수할 수 있을 만큼인 귀하의 새로운 기술과 지식을, 이수증으로 보여주세요. 이수증을 귀하의 커리어 개발과 학교" +" 지원에 활용하면 도움이 될 것입니다." #: common/templates/course_modes/choose.html msgid "Benefits of a Verified Certificate" -msgstr "인증된 강좌 수료증의 혜택" +msgstr "인증된 이수증의 혜택" #: common/templates/course_modes/choose.html msgid "" @@ -9593,7 +9611,7 @@ msgstr "{b_start} credit을 위한 자격:{b_end} 강좌를 성공적으로 완 msgid "" "{b_start}Official:{b_end} Receive an instructor-signed certificate with the " "institution's logo" -msgstr "{b_start}공식:{b_end} 기관의 로고와 교수자의 서명이 담긴 강좌 수료증을 받으세오." +msgstr "{b_start}공식:{b_end} 기관의 로고와 교수자의 서명이 담긴 이수증을 받으세요." #: common/templates/course_modes/choose.html msgid "" @@ -9692,7 +9710,7 @@ msgstr "수강중인 강좌 " #: lms/templates/dashboard.html msgid "Looks like you haven't enrolled in any courses yet." -msgstr "아직 등록한 강의가 없군요!" +msgstr "아직 수강하는 강의가 없군요!" #: lms/templates/dashboard.html msgid "Find courses now!" @@ -10776,7 +10794,7 @@ msgstr "사용자" #: lms/templates/sysadmin_dashboard.html #: lms/templates/sysadmin_dashboard_gitlogs.html msgid "Staffing and Enrollment" -msgstr "운영진 설정 및 등록" +msgstr "운영팀 설정 및 등록" #. Translators: refers to http://git-scm.com/docs/git-log #: lms/templates/sysadmin_dashboard.html @@ -10820,7 +10838,7 @@ msgstr "강좌 운영팀과 교수자를 관리합니다." #: lms/templates/sysadmin_dashboard.html msgid "Download staff and instructor list (csv file)" -msgstr "운영진/교수자 목록 다운로드(CSV파일)" +msgstr "운영팀/교수자 목록 다운로드(CSV파일)" #: lms/templates/sysadmin_dashboard.html msgid "Administer Courses" @@ -11215,7 +11233,7 @@ msgstr "학습자 일괄 추가를 위해 줄바꿈 혹은 쉼표로 이메일/ msgid "" "You will not get notification for emails that bounce, so please double-check" " spelling." -msgstr "이메일 주소가 정확한지 확인해야 합니다. 반송되는 이메일을 확인할 수 없기 때문입니다." +msgstr "이메일 주소가 정확한지 확인해야 합니다. " #: lms/templates/ccx/enrollment.html #: lms/templates/instructor/instructor_dashboard_2/membership.html @@ -11754,7 +11772,8 @@ msgstr "선수요건" msgid "" "You must successfully complete {link_start}{prc_display}{link_end} before " "you begin this course." -msgstr "강좌를 시작하시 전에 {link_start}{prc_display}{link_end}을 성공적으로 완료해야 합니다. " +msgstr "" +"이 강좌를 수강하려면, 먼저 선수 강좌인 {link_start}{prc_display}{link_end} 수강을 하셔야 합니다." #: lms/templates/courseware/course_about.html msgid "Additional Resources" @@ -11849,7 +11868,7 @@ msgstr "검색" #: lms/templates/courseware/courseware.html msgid "No content has been added to this course" -msgstr "강좌 내용을 추가해 보세요. " +msgstr "강좌 내용을 추가하세요. " #: lms/templates/courseware/courseware.html #, python-format @@ -12068,8 +12087,10 @@ msgid "Section:" msgstr "주제:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "문제 url 이름:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -13850,7 +13871,7 @@ msgid "" "exams and problem sets), and can be changed on the 'Grading' page (under " "'Settings') in Studio." msgstr "" -"강좌의 채점 정보를 표시하려면 클릭하세요. 스튜디오의 '과제 평가' ('설정' 메뉴 아래) 메뉴에서 설정(문제 세트, 평가 등)을 변경할" +"강좌의 성적 정보를 표시하려면 클릭하세요. 스튜디오의 '과제 평가' ('설정' 메뉴 아래) 메뉴에서 설정(문제 세트, 평가 등)을 변경할" " 수 있습니다." #: lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -13863,7 +13884,7 @@ msgstr "익명 학습자 아이디를 CSV 파일로 다운로드하기" #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "Get Student Anonymized IDs CSV" -msgstr "학습자 익명 아이디를 CSV 파일 만들기" +msgstr "익명 학습자 아이디 CSV 파일" #: lms/templates/instructor/instructor_dashboard_2/data_download.html #: lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -13885,7 +13906,7 @@ msgstr "" msgid "" "Please be patient and do not click these buttons multiple times. Clicking " "these buttons multiple times will significantly slow the generation process." -msgstr "잠시만 기다려 주세요. 버튼을 여러번 클릭하지 마세요. 버튼을 여러번 클릭하게 되면 생성하는 과정이 느려집니다." +msgstr "버튼을 여러번 클릭하지 마세요. 그럴 경우, 생성 과정이 느려집니다." #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" @@ -13916,6 +13937,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -13950,8 +13985,8 @@ msgid "" "generation. Reports are not deleted, so you will always be able to access " "previously generated reports from this page." msgstr "" -"아래 보고서를 다운로드할 수 있습니다. 각 보고서는 UTC 날짜와 생성시간에 의해 구별됩니다. 생성된 보고서는 지워지지 않기 때문에, " -"계속 다운로드 가능합니다." +"아래 보고서를 다운로드할 수 있습니다. 각 보고서는 날짜와 생성시간으로 구별됩니다. 생성된 보고서는 지워지지 않기 때문에, 계속 다운로드" +" 가능합니다." #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" @@ -14664,8 +14699,8 @@ msgid "" " to manage course team membership. You can only give course team roles to " "enrolled users." msgstr "" -"관리자 권한을 가진 강좌 팀원은 당신의 강좌 운영을 지원합니다. 운영팀이 할 수 있는 모든 작업을 할 수 있고, 강좌 팀원을 관리하기 " -"위해 운영팀, 관리자, 토의 진행자, 베타테스터 권한을 추가하거나 없앨수 있습니다. " +"교수자 권한을 가진 팀원은 강좌 운영팀이 할 수 있는 모든 작업을 할 수 있고, 팀 구성원을 관리하기 위해 운영팀, 교수자, 토의 " +"조정자, 베타테스터 권한을 추가하거나 없앨 수 있습니다. " #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Add Admin" @@ -14848,7 +14883,7 @@ msgstr "" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Send Email" -msgstr "이메일 발송" +msgstr "이메일 발신" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Send to:" @@ -14882,7 +14917,7 @@ msgstr "본문: " msgid "" "Please try not to email students more than once per week. Before sending " "your email, consider:" -msgstr "주 1회 이상 이메일 발송은 삼가해 주세요. 이메일 발송 전 점검 사항:" +msgstr "주 1회 이상 이메일 발신은 삼가해 주세요. 이메일 발신 전 점검 사항:" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "" @@ -14895,7 +14930,7 @@ msgid "" "Have you sent the email to yourself first to make sure you're happy with how" " it's displayed, and that embedded links and images work properly?" msgstr "" -"이메일이 잘 작성되었는지 확인하기 위해 우선 나에게만 발송하셨는지요? 이메일 내용 중 링크와 이미지는 잘 보이는지 확인하세요." +"이메일이 잘 작성되었는지 확인하기 위해 우선 자신에게만 발신하셨는지요? 이메일 내용 중 링크와 이미지는 잘 보이는지 확인하세요." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "CAUTION!" @@ -14905,11 +14940,11 @@ msgstr "주의!" msgid "" "Once the 'Send Email' button is clicked, your email will be queued for " "sending." -msgstr "일단 아래 '이메일 발송' 을 클릭하면, 발송대기 상태로 바뀝니다. " +msgstr "일단 아래 '이메일 발신' 을 클릭하면, 발신 대기 상태로 바뀝니다. " #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "A queued email CANNOT be cancelled." -msgstr "발송한 메일은 취소되지 않습니다." +msgstr "발신은 취소할 수 없습니다." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "" @@ -14919,15 +14954,15 @@ msgstr "모든 작업은 백그라운드에서 이루어집니다. 이메일을 #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Email Task History" -msgstr "이메일 기록" +msgstr "이메일 발신 기록" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "To see the content of all previously sent emails, click this button:" -msgstr "발송된 이메일 내용을 보기 위해 다음 버튼을 클릭하세요." +msgstr "발신된 이메일 내용을 보기 위해 다음 버튼을 클릭하세요." #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "Sent Email History" -msgstr "이메일 기록 보기" +msgstr "이메일 발신 기록 보기" #: lms/templates/instructor/instructor_dashboard_2/send_email.html msgid "To read an email, click its subject." @@ -14971,7 +15006,7 @@ msgstr "성적 기록부 보기 " #: lms/templates/instructor/instructor_dashboard_2/student_admin.html msgid "Student-specific grade inspection" -msgstr "학습자 별 성적 사정" +msgstr "학습자별 성적 사정" #: lms/templates/instructor/instructor_dashboard_2/student_admin.html msgid "Click this link to view the student's progress page:" @@ -15990,7 +16025,7 @@ msgstr "" #: lms/templates/verify_student/face_upload.html msgid "Edit Your Full Name" -msgstr "이름 편집" +msgstr "실명 편집" #: lms/templates/verify_student/face_upload.html msgid "The following error occurred while editing your name:" @@ -16212,7 +16247,7 @@ msgstr "페이지 액션" #: cms/templates/asset_index.html cms/templates/videos_index.html msgid "Upload New File" -msgstr "새 파일 업로드" +msgstr "파일 업로드" #: cms/templates/asset_index.html msgid "Adding Files for Your Course" @@ -16223,7 +16258,7 @@ msgid "" "To add files to use in your course, click {em_start}Upload New File{em_end}." " Then follow the prompts to upload a file from your computer." msgstr "" -"강좌에 사용할 파일을 추가하려면 {em_start}새 파일 업로드{em_end}를 클릭하세요. 그후 컴퓨터에서 파일을 업로드하세요." +"강좌에 사용할 파일을 추가하려면 {em_start} 파일 업로드{em_end}를 클릭하세요. 그후 컴퓨터에서 파일을 업로드하세요." #: cms/templates/asset_index.html msgid "" @@ -16288,65 +16323,62 @@ msgstr "알림 닫기" #: cms/templates/certificates.html msgid "Course Certificates" -msgstr "강좌 수료증" +msgstr "이수증" #: cms/templates/certificates.html msgid "This module is not enabled." msgstr "모듈이 활성화되지 않았습니다." #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" -"강좌 수료 직후, 학습자는 성과를 인정받을 수 있는 수료증을 받을 수 있습니다. 강좌 운영팀 중 교수자가 수료증을 설정할 수 있습니다." -" " #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" -"새로운 수료증 설정을 위해 {em_start}첫 수료증 추가하기{em_end}를 클릭합니다. 수료증에 넣을 기관 로고를 업로드하고, " -"최소 1개 이상의 서명인을 지정하세요. 최대 4개까지 지정 가능합니다. 서명인으로 사용할 이미지 파일을 각각 업로드할 수 있습니다. " -"{em_start} 알아두셔야 할 것은, {em_end} 서명 이미지가 인증 수료증에만 사용된다는 것입니다. 선택 사항입니다만, 각 " -"수료증마다 강좌명을 다르게 할 수도 있습니다. 이는 예를 들어, 실제 강좌명이 수료증에 넣기에 너무 긴 경우 등을 위한 것입니다." #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" -"강좌 모드를 선택하고, 선택한 수강 트랙의 학습자가 받을 수료증을 미리보기 위해, {em_start}수료증 미리보기{em_end}를 " -"클릭합니다. 수료증 발급 준비가 되셨다면, {em_start}활성화{em_end}를 클릭하세요. 수료증 발급 활성화를 중단하려면, " -"{em_start}비활성화{em_end}를 클릭합니다." #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" -"수료증 설정을 수정하려면, 양식 오른쪽 상단의 {em_start}수정{em_end}을 클릭하세요. 수료증을 삭제하려면, 삭제 아이콘을 " -"클릭하면 됩니다. 이미 강좌가 시작된 경우라면, 수료증을 삭제하지 마십시오. 이미 일부 학습자에게 수료증이 발급되었을 수 있기 " -"때문입니다. " #: cms/templates/certificates.html msgid "Learn more about certificates" -msgstr "강좌 수료증에 대해 더 알아보기" +msgstr "이수증에 대해 더 알아보기" #: cms/templates/certificates.html cms/templates/group_configurations.html #: cms/templates/settings.html cms/templates/settings_advanced.html @@ -16648,7 +16680,7 @@ msgstr "강좌 공지사항" #: cms/templates/course_info.html msgid "New Update" -msgstr "신규 업데이트" +msgstr "새 공지사항" #: cms/templates/course_info.html msgid "" @@ -16801,7 +16833,7 @@ msgid "" "To hide content from students, select the Configure icon for a section, " "subsection, or unit, then select {em_start}Hide from students{em_end}." msgstr "" -"학습자에게 콘텐츠를 보이지 않게 하려면, 주제 및 소주제의 설정에서 {em_start}학습자에게 감추기기{em_end}를 선택하세요." +"학습자에게 콘텐츠를 보이지 않게 하려면, 주제 및 소주제의 설정에서 {em_start}학습자에게 감추기{em_end}를 선택하세요." #: cms/templates/course_outline.html msgid "Learn more about the course outline" @@ -17759,7 +17791,9 @@ msgstr "고급 설정에서 학습자가 볼 명칭을 새롭게 설정할 수 #: cms/templates/index.html msgid "" "The unique number that identifies your course within your organization." -msgstr "기관별 강좌 고유번호입니다." +msgstr "" +"이 강좌의 고유한 번호로 'KMOOC01'과 같이, 강좌 제공 기관의 영문 약자와 제공 기관의 몇 번째 강좌인지 번호의 조합으로 " +"입력합니다. " #: cms/templates/index.html msgid "" @@ -17770,7 +17804,7 @@ msgstr "" #: cms/templates/index.html msgid "The term in which your course will run." -msgstr "강좌 운영 기간." +msgstr "강좌 제공 기관별로 자유롭게 번호를 지정할 수 있습니다. " #: cms/templates/index.html msgid "Create" @@ -17877,17 +17911,17 @@ msgstr "강의 다시 시작하기" #: cms/templates/index.html msgid "Are you staff on an existing {studio_name} course?" -msgstr "{studio_name} 관리자입니까?" +msgstr "{studio_name} 강좌 운영팀인가요?" #: cms/templates/index.html msgid "" "The course creator must give you access to the course. Contact the course " "creator or administrator for the course you are helping to author." -msgstr "강좌 개설자가 귀하에게 강좌 접근 권한을 줘야 합니다. 강좌 개설자 또는 관리자에게 문의하세요." +msgstr "강좌 개설자가 귀하에게 강좌 접근 권한을 줘야 합니다. 강좌 개설자 또는 강좌 제공 기관의 관리자에게 문의하세요." #: cms/templates/index.html msgid "Create Your First Course" -msgstr "처음 강좌를 만들어 보세요" +msgstr "첫 강좌를 만듭니다." #: cms/templates/index.html msgid "Your new course is just a click away!" @@ -17960,7 +17994,7 @@ msgstr "" msgid "" "Your request is currently being reviewed by {platform_name} staff and should" " be updated shortly." -msgstr "요청은 현재 {platform_name} 직원에 의해 검토되고 있으며 곧 업데이트 될 것입니다." +msgstr "요청은 현재 {platform_name} 관리자에 의해 검토되고 있으며 곧 업데이트 될 것입니다." #: cms/templates/index.html msgid "(Read-only)" @@ -18055,7 +18089,7 @@ msgid "" "email address (%(email)s). An activation message and next steps should be " "waiting for you there." msgstr "" -"등록 절차를 완료하려면 이메일 주소(%(email)s)를 인증받아야 합니다. 입력한 이메일 주소로 발송된 등록 이메일을 확인해주세요." +"등록 절차를 완료하려면 이메일 주소(%(email)s)를 인증받아야 합니다. 입력한 이메일 주소로 발신된 등록 이메일을 확인해주세요." #: cms/templates/index.html msgid "Need help?" @@ -18172,7 +18206,7 @@ msgstr "새로 추가할 구성원 이메일 주소" #: cms/templates/manage_users.html msgid "Provide the email address of the user you want to add as Staff" -msgstr "관리자로 추가하고 싶은 이용자의 이메일 주소를 제공합니다." +msgstr "운영팀으로 추가하고 싶은 이용자의 이메일 주소를 입력합니다." #: cms/templates/manage_users.html cms/templates/manage_users_lib.html msgid "Add User" @@ -18561,7 +18595,7 @@ msgstr "수강 대상 학습자를 위한 정보" #: cms/templates/settings.html msgid "Course Short Description" -msgstr "강좌 간단소개" +msgstr "강좌에 대한 짧은 소개" #: cms/templates/settings.html msgid "" @@ -18641,11 +18675,11 @@ msgstr "효과적인 학습을 위해 학습 시간 및 선수 강좌를 입력 #: cms/templates/settings.html msgid "Hours of Effort per Week" -msgstr "주당 학습 권장 시간" +msgstr "주별 학습 권장 시간" #: cms/templates/settings.html msgid "Time spent on all course work" -msgstr "주당 학습에 투자하길 권장하는 시간" +msgstr "주별 학습에 투자하길 권장하는 시간" #: cms/templates/settings.html msgid "Prerequisite Course" @@ -19011,7 +19045,7 @@ msgstr "공개 대상:" #: cms/templates/visibility_editor.html msgid "All Students and Staff" -msgstr "전체 학습자와 관리자" +msgstr "전체 학습자와 운영팀" #: cms/templates/visibility_editor.html msgid "Specific Content Groups" @@ -19047,8 +19081,7 @@ msgid "" "Thank you for signing up for {studio_name}! To activate your account, please" " copy and paste this address into your web browser's address bar:" msgstr "" -"{studio_name}에 가입 해 주셔서 감사합니다! 계정을 활성화 하려면 웹 브라우저의 주소 표시 줄에 이 주소를 복사해서 붙여 " -"넣으십시오" +"{studio_name} 가입을 환영합니다! 계정을 활성화하려면 웹 브라우저의 주소 표시줄에 이 주소를 복사해서 붙여넣으시길 바랍니다." #: cms/templates/emails/activation_email.txt msgid "" diff --git a/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo b/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo index c4b6b78e49..b87fd7f358 100644 Binary files a/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo and b/conf/locale/ko_KR/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ko_KR/LC_MESSAGES/djangojs.po b/conf/locale/ko_KR/LC_MESSAGES/djangojs.po index 09ea189121..c3770e81f5 100644 --- a/conf/locale/ko_KR/LC_MESSAGES/djangojs.po +++ b/conf/locale/ko_KR/LC_MESSAGES/djangojs.po @@ -53,9 +53,9 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 13:15+0000\n" -"Last-Translator: Hongseob Lee \n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" +"Last-Translator: Sarina Canelake \n" "Language-Team: Korean (Korea) (http://www.transifex.com/open-edx/edx-platform/language/ko_KR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -100,8 +100,8 @@ msgstr "확인" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -112,6 +112,11 @@ msgstr "확인" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "취소" @@ -2006,6 +2011,18 @@ msgstr "코멘트를 삭제하는데 오류가 발생했습니다. 다시 시도 msgid "Are you sure you want to delete this response?" msgstr "이 답변을 삭제하시겠습니까?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "총 %(num_items)s개 중 %(first_index)s 보기" @@ -2163,6 +2180,7 @@ msgstr "게시된 날짜" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2234,21 +2252,35 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "이름" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "open_slots" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "팀 인원 수" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2273,6 +2305,7 @@ msgid "Language" msgstr "언어" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2283,6 +2316,7 @@ msgid "Country" msgstr "국가" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2315,20 +2349,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "%(span_start)s %(team_name)s %(span_end)s 보기" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "확인" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2345,37 +2407,62 @@ msgstr "" msgid "teams" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "팀" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" " with other learners who are interested in the same topic as you are." msgstr "강좌에서 주제별로 조직된 모든 팀을 보세요. 관심사가 같은 다른 학습자들과 협력하기 위해 팀에 참여하세요." -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "팀" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2397,15 +2484,27 @@ msgid "%(team_count)s Team" msgid_plural "%(team_count)s Teams" msgstr[0] "%(team_count)s 팀" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "%(topic_name)s 주제의 팀 보기" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -2605,7 +2704,7 @@ msgstr "다음 이용자들을 성공적으로 등록했습니다." msgid "" "Successfully sent enrollment emails to the following users. They will be " "allowed to enroll once they register:" -msgstr "성공적으로 등록 이메일을 다음 이용자들에게 발송했습니다. 이용자들이 등록을 하면 .. 용어 확인이 필요합니다." +msgstr "등록 이메일을 다음 이용자들에게 발송했습니다. " #. Translators: A list of users appears after this sentence; #: lms/static/coffee/src/instructor_dashboard/membership.js @@ -2987,7 +3086,7 @@ msgstr "본문: " #: lms/static/coffee/src/instructor_dashboard/util.js msgid "No tasks currently running." -msgstr "실행중인 작업이 없습니다." +msgstr "작업이 없습니다." #: lms/static/coffee/src/instructor_dashboard/util.js msgid "File Name" @@ -3527,7 +3626,7 @@ msgstr "{platform_name}에서 귀하를 나타낼 이름입니다. 아이디는 #: lms/static/js/student_account/views/account_settings_factory.js msgid "Full Name" -msgstr "이름" +msgstr "실명" #: lms/static/js/student_account/views/account_settings_factory.js msgid "" @@ -3763,11 +3862,6 @@ msgstr "신분증 사진을 찍어 주세요." msgid "Review your info" msgstr "입력 정보를 확인하세요." -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "확인" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "오류가 발생했습니다. 페이지를 다시 불러오세요." @@ -4309,6 +4403,7 @@ msgid "Date Added" msgstr "" #: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore msgid "Type" msgstr "" @@ -4338,7 +4433,7 @@ msgstr "" #: cms/static/js/views/assets.js cms/static/js/views/assets.js.c #: cms/templates/js/asset-upload-modal.underscore msgid "Upload New File" -msgstr "" +msgstr "파일 업로드" #: cms/static/js/views/assets.js cms/static/js/views/assets.js.c msgid "Load Another File" @@ -4862,18 +4957,6 @@ msgid "" "more than <%=limit%> characters." msgstr "" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "영어 알파벳을 입력하고, 공백이나 특수문자를 사용하지 마세요." - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "" @@ -4963,11 +5046,526 @@ msgstr "" msgid "Due Date" msgstr "" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "상태" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "기부" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5008,12 +5606,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5103,10 +5695,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "기부" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "" @@ -5236,11 +5824,11 @@ msgstr "신규 학습 집단 추가" #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Enter the name of the cohort" -msgstr "" +msgstr "학습집단명을 입력하세요." #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cohort Name" -msgstr "학습 집단명" +msgstr "학습집단명" #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cohort Assignment Method" @@ -5538,7 +6126,7 @@ msgstr "" #: lms/templates/student_account/register.underscore msgid "Create an account using" -msgstr "가입하기" +msgstr "계정 연동하기" #: lms/templates/student_account/register.underscore #, python-format @@ -6021,6 +6609,16 @@ msgstr "" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "" @@ -6095,6 +6693,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6139,19 +6742,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6263,11 +6860,11 @@ msgstr "" #: cms/templates/js/course-outline.underscore #: cms/templates/js/publish-xblock.underscore msgid "Unscheduled" -msgstr "" +msgstr "지정되지 않음" #: cms/templates/js/course-outline.underscore msgid "Graded as:" -msgstr "" +msgstr "과제 유형" #: cms/templates/js/course-outline.underscore msgid "Due:" @@ -6436,7 +7033,7 @@ msgstr "" #: cms/templates/js/grading-editor.underscore msgid "Grade as:" -msgstr "" +msgstr "과제 유형" #: cms/templates/js/group-configuration-details.underscore #: cms/templates/js/group-configuration-editor.underscore @@ -6576,10 +7173,6 @@ msgstr "" msgid "Add your first textbook" msgstr "" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/pt_BR/LC_MESSAGES/django.mo b/conf/locale/pt_BR/LC_MESSAGES/django.mo index 9d2de4108f..4d012ef644 100644 Binary files a/conf/locale/pt_BR/LC_MESSAGES/django.mo and b/conf/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/conf/locale/pt_BR/LC_MESSAGES/django.po b/conf/locale/pt_BR/LC_MESSAGES/django.po index 49080c44c3..b38de5e4ad 100644 --- a/conf/locale/pt_BR/LC_MESSAGES/django.po +++ b/conf/locale/pt_BR/LC_MESSAGES/django.po @@ -17,6 +17,7 @@ # Francisco Cantarutti , 2014 # G.Ribas , 2014 # Guilherme Batista Ferreira , 2015 +# Guilherme Tadiello , 2015 # Gustavo Henrique de Almeida Gonçalves , 2015 # Gustavo Henrique de Almeida Gonçalves , 2015 # Heitor Althmann , 2014 @@ -111,6 +112,7 @@ # G.Ribas , 2014 # Gilson Leite Siqueira Junior , 2014 # Gislene Kucker Arantes , 2014 +# Guilherme Tadiello , 2015 # Gustavo Henrique de Almeida Gonçalves , 2015 # Gustavo Henrique de Almeida Gonçalves , 2015 # Hudson Martins dos Santos , 2015 @@ -223,7 +225,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-07-20 00:15+0000\n" "Last-Translator: javiercencig \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/open-edx/edx-platform/language/pt_BR/)\n" @@ -1386,10 +1388,6 @@ msgstr "certa" msgid "incorrect" msgstr "errada" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "incompleta" @@ -1412,10 +1410,6 @@ msgstr "" msgid "This is incorrect." msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -4955,7 +4949,14 @@ msgid "{month} {day}, {year}" msgstr "" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -5055,13 +5056,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -5220,6 +5221,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5758,6 +5767,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -6148,6 +6174,10 @@ msgstr "" msgid "CourseMode price updated successfully" msgstr "" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -6242,18 +6272,6 @@ msgstr "E-mail externo" msgid "Grades for assignment \"{name}\"" msgstr "Notas da tarefa \"{name}\"" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "Foram encontrados {num} registros a serem excluídos." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "Não foi possível encontrar o módulo com este nome de url." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "Status do estudante para o problema {problem}" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "Notas do curso {course_id}" @@ -6423,6 +6441,12 @@ msgstr "excluído" msgid "emailed" msgstr "E-mail enviado" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6435,12 +6459,6 @@ msgstr "avaliado" msgid "problem distribution graded" msgstr "problema de distribuição avaliado" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7908,6 +7926,7 @@ msgid "Optional language the team uses as ISO 639-1 code." msgstr "" #: lms/djangoapps/teams/plugins.py +#: lms/djangoapps/teams/templates/teams/teams.html msgid "Teams" msgstr "" @@ -7920,11 +7939,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9680,6 +9699,10 @@ msgstr "Ajuda" msgid "Sign Out" msgstr "" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "" @@ -12293,8 +12316,10 @@ msgid "Section:" msgstr "Seção:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "Nome do URL do problema:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -14187,6 +14212,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -16604,41 +16643,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html diff --git a/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo b/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo index 8a76fe7434..836ed8adac 100644 Binary files a/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo and b/conf/locale/pt_BR/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po index 6bbec3370f..520b7ea46d 100644 --- a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po +++ b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -112,6 +112,7 @@ # Francisco de Assis Ventura Filho , 2015 # G , 2014 # Guilherme Henrique Spiller , 2015 +# Gustavo Bertoli, 2015 # Hudson Martins dos Santos , 2015 # Jaqueline Knebel Wildner , 2015 # Leonardo Flores Zambaldi , 2014 @@ -151,8 +152,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/open-edx/edx-platform/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -197,8 +198,8 @@ msgstr "OK" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js msgid "Cancel" msgstr "Cancelar" @@ -2124,6 +2125,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "Você tem certeza de que deseja apagar esta resposta?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "Exibindo %(first_index)s de um total de %(num_items)s" @@ -2358,13 +2371,16 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -#: lms/djangoapps/teams/static/teams/js/collections/topic.js -#: lms/templates/edxnotes/tab-item.underscore -msgid "name" +msgid "last activity" msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" +msgid "open slots" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/topic.js +#: lms/templates/edxnotes/tab-item.underscore +msgid "name" msgstr "" #. Translators: This refers to the number of teams (a count of how many teams @@ -2373,6 +2389,17 @@ msgstr "" msgid "team count" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2397,6 +2424,7 @@ msgid "Language" msgstr "Idioma" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2407,6 +2435,7 @@ msgid "Country" msgstr "País" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2439,20 +2468,47 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +msgid "Confirm" +msgstr "Confirmar" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2471,13 +2527,13 @@ msgid "teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"See all teams in your course, organized by topic. Join a team to collaborate" -" with other learners who are interested in the same topic as you are." +msgid "Teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" +msgid "" +"See all teams in your course, organized by topic. Join a team to collaborate" +" with other learners who are interested in the same topic as you are." msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -2485,22 +2541,47 @@ msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2523,15 +2604,27 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "" msgstr[1] "" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -3954,10 +4047,6 @@ msgstr "Tire uma foto do seu documento de identidade" msgid "Review your info" msgstr "Revise suas informações" -#: lms/static/js/verify_student/views/reverify_view.js -msgid "Confirm" -msgstr "Confirmar" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "Ocorreu um erro. Por favor, tente atualizar a página." @@ -4327,6 +4416,7 @@ msgstr "" #: cms/static/js/factories/manage_users.js #: cms/static/js/factories/manage_users_lib.js +#: common/static/common/templates/discussion/post-user-display.underscore msgid "Staff" msgstr "" @@ -4516,6 +4606,7 @@ msgid "Date Added" msgstr "" #: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore msgid "Type" msgstr "" @@ -5074,18 +5165,6 @@ msgid "" "more than <%=limit%> characters." msgstr "" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "" @@ -5175,11 +5254,526 @@ msgstr "" msgid "Due Date" msgstr "" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5220,12 +5814,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5315,10 +5903,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "" @@ -6235,6 +6819,16 @@ msgstr "" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "" @@ -6309,6 +6903,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6353,19 +6952,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6790,10 +7383,6 @@ msgstr "" msgid "Add your first textbook" msgstr "" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/rtl/LC_MESSAGES/django.mo b/conf/locale/rtl/LC_MESSAGES/django.mo index 12c894a5e3..994257b241 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/django.mo and b/conf/locale/rtl/LC_MESSAGES/django.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/django.po b/conf/locale/rtl/LC_MESSAGES/django.po index 1f2b947193..14418d68a1 100644 --- a/conf/locale/rtl/LC_MESSAGES/django.po +++ b/conf/locale/rtl/LC_MESSAGES/django.po @@ -37,8 +37,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-24 22:03+0000\n" -"PO-Revision-Date: 2015-08-24 22:03:48.240461\n" +"POT-Creation-Date: 2015-09-04 14:15+0000\n" +"PO-Revision-Date: 2015-09-04 14:15:47.324123\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -1253,10 +1253,6 @@ msgstr "ذخققثذف" msgid "incorrect" msgstr "هرذخققثذف" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "حشقفهشممغ ذخققثذف" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "هرذخوحمثفث" @@ -1279,10 +1275,6 @@ msgstr "فاهس هس ذخققثذف." msgid "This is incorrect." msgstr "فاهس هس هرذخققثذف." -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "فاهس هس حشقفهشممغ ذخققثذف." - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "فاهس هس عرشرسصثقثي." @@ -5028,8 +5020,18 @@ msgid "{month} {day}, {year}" msgstr "{month} {day}, {year}" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." -msgstr "ش ذخعقسث خب سفعيغ خببثقثي زغ {partner_name}, فاقخعلا {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" +"ش ذخعقسث خب سفعيغ خببثقثي زغ {partner_short_name}, شر خرمهرث مثشقرهرل " +"هرهفهشفهدث خب {partner_long_name} فاقخعلا {platform_name}." + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." +msgstr "" +"ش ذخعقسث خب سفعيغ خببثقثي زغ {partner_short_name}, فاقخعلا {platform_name}." #. Translators: Accomplishments describe the awards/certifications obtained by #. students on this platform @@ -5141,16 +5143,16 @@ msgstr "{platform_name} شذنرخصمثيلثس فاث بخممخصهرل سف #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" "فاهس هس ش دشمهي {platform_name} ذثقفهبهذشفث بخق {user_name}, صاخ " -"حشقفهذهحشفثي هر {partner_name} {course_number}" +"حشقفهذهحشفثي هر {partner_short_name} {course_number}" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" -msgstr "{partner_name} {course_number} ذثقفهبهذشفث | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" +msgstr "{partner_short_name} {course_number} ذثقفهبهذشفث | {platform_name}" #. Translators: This text fragment appears after the student's name #. (displayed in a large font) on the certificate @@ -5319,6 +5321,14 @@ msgstr "" "هب غخعق ذخعقسث يخثس رخف شححثشق خر غخعق يشسازخشقي, ذخرفشذف " "{payment_support_link}." +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "{course_id} هس رخف ش دشمهي ذخعقسث نثغ." + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "ذخعقسث {course_id} يخثس رخف ثطهسف." + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5864,6 +5874,28 @@ msgstr "عسثقرشوث {user} شمقثشيغ ثطهسفس." msgid "File is not attached." msgstr "بهمث هس رخف شففشذاثي." +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "ذخعمي رخف بهري حقخزمثو صهفا فاهس مخذشفهخر." + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" +"فاث حقخزمثو قثسحخرسثس قثحخقف هس زثهرل ذقثشفثي. فخ دهثص فاث سفشفعس خب فاث " +"قثحخقف, سثث حثريهرل فشسنس زثمخص." + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" +"ش حقخزمثو قثسحخرسثس قثحخقف لثرثقشفهخر فشسن هس شمقثشيغ هر حقخلقثسس. ذاثذن فاث" +" 'حثريهرل فشسنس' فشزمث بخق فاث سفشفعس خب فاث فشسن. صاثر ذخوحمثفثي, فاث " +"قثحخقف صهمم زث شدشهمشزمث بخق يخصرمخشي هر فاث فشزمث زثمخص." + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "هردخهذث رعوزثق '{num}' يخثس رخف ثطهسف." @@ -6292,6 +6324,10 @@ msgstr "ذخعقسثوخيث صهفا فاث وخيث سمعل({mode_slug}) يخ msgid "CourseMode price updated successfully" msgstr "ذخعقسثوخيث حقهذث عحيشفثي سعذذثسسبعممغ" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "رخ ثري يشفث سثف" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "ثرقخمموثرف يشفش هس رخص شدشهمشزمث هر {dashboard_link}." @@ -6389,18 +6425,6 @@ msgstr "ثطفثقرشم ثوشهم" msgid "Grades for assignment \"{name}\"" msgstr "لقشيثس بخق شسسهلروثرف \"{name}\"" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "بخعري {num} قثذخقيس فخ يعوح." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "ذخعمير'ف بهري وخيعمث صهفا فاشف عقمرشوث." - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "سفعيثرف سفشفث بخق حقخزمثو {problem}" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "لقشيثس بقخو {course_id}" @@ -6569,6 +6593,12 @@ msgstr "يثمثفثي" msgid "emailed" msgstr "ثوشهمثي" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "لثرثقشفثي" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6581,12 +6611,6 @@ msgstr "لقشيثي" msgid "problem distribution graded" msgstr "حقخزمثو يهسفقهزعفهخر لقشيثي" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "لثرثقشفثي" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -9457,15 +9481,6 @@ msgstr "سشدث بشهمثي بخق عسثق حقثبثقثرذث '{key}' صه msgid "No data provided for user preference update" msgstr "رخ يشفش حقخدهيثي بخق عسثق حقثبثقثرذث عحيشفث" -#: openedx/core/lib/api/paginators.py -msgid "Page is not 'last', nor can it be converted to an int." -msgstr "حشلث هس رخف 'مشسف', رخق ذشر هف زث ذخردثقفثي فخ شر هرف." - -#: openedx/core/lib/api/paginators.py -#, python-format -msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "هردشمهي حشلث (%(page_number)s): %(message)s" - #: openedx/core/lib/api/view_utils.py msgid "This value is invalid." msgstr "فاهس دشمعث هس هردشمهي." @@ -12753,8 +12768,12 @@ msgid "Section:" msgstr "سثذفهخر:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "حقخزمثو عقمرشوث:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" +"فخ يخصرمخشي ش ذسد مهسفهرل سفعيثرف قثسحخرسثس فخ ش لهدثر حقخزمثو, دهسهف فاث " +"يشفش يخصرمخشي سثذفهخر خب فاث هرسفقعذفخق يشسازخشقي." #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -14764,6 +14783,22 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "لثرثقشفث حقخذفخقثي ثطشو قثسعمفس قثحخقف" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" +"فخ لثرثقشفث ش ذسد بهمث فاشف مهسفس شمم سفعيثرف شرسصثقس فخ ش لهدثر حقخزمثو, " +"ثرفثق فاث مخذشفهخر خب فاث حقخزمثو (بقخو هفس سفشبب يثزعل هربخ)." + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "حقخزمثو مخذشفهخر: " + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "يخصرمخشي ش ذسد خب حقخزمثو قثسحخرسثس" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.mo b/conf/locale/rtl/LC_MESSAGES/djangojs.mo index f671e3f3ce..6cffe673b2 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/djangojs.mo and b/conf/locale/rtl/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.po b/conf/locale/rtl/LC_MESSAGES/djangojs.po index d42d82dda1..d39887470b 100644 --- a/conf/locale/rtl/LC_MESSAGES/djangojs.po +++ b/conf/locale/rtl/LC_MESSAGES/djangojs.po @@ -26,8 +26,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-24 22:03+0000\n" -"PO-Revision-Date: 2015-08-24 22:03:48.948707\n" +"POT-Creation-Date: 2015-09-04 14:15+0000\n" +"PO-Revision-Date: 2015-09-04 14:15:47.640544\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "MIME-Version: 1.0\n" @@ -73,8 +73,8 @@ msgstr "خن" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -89,6 +89,7 @@ msgstr "خن" #: common/static/common/templates/discussion/response-comment-edit.underscore #: common/static/common/templates/discussion/thread-edit.underscore #: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "ذشرذثم" @@ -2084,6 +2085,18 @@ msgstr "صث اشي سخوث فقخعزمث يثمثفهرل فاهس ذخووث msgid "Are you sure you want to delete this response?" msgstr "شقث غخع سعقث غخع صشرف فخ يثمثفث فاهس قثسحخرسث?" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "قثضعهقثي بهثمي." + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "حمثشسث يخ رخف عسث شرغ سحشذثس هر فاهس بهثمي." + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "حمثشسث يخ رخف عسث شرغ سحشذثس خق سحثذهشم ذاشقشذفثقس هر فاهس بهثمي." + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "ساخصهرل %(first_index)s خعف خب %(num_items)s فخفشم" @@ -2268,6 +2281,7 @@ msgid "Public" msgstr "حعزمهذ" #: common/static/js/vendor/ova/catch/js/catch.js +#: common/static/common/templates/components/search-field.underscore #: lms/djangoapps/support/static/support/templates/certificates.underscore msgid "Search" msgstr "سثشقذا" @@ -2330,21 +2344,35 @@ msgid "An unexpected error occurred. Please try again." msgstr "شر عرثطحثذفثي ثققخق خذذعققثي. حمثشسث فقغ شلشهر." #: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "مشسف شذفهدهفغ" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "خحثر سمخفس" + #: lms/djangoapps/teams/static/teams/js/collections/topic.js #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "رشوث" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "خحثر_سمخفس" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "فثشو ذخعرف" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "ذقثشفث" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "عحيشفث" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "فثشو رشوث (قثضعهقثي) *" @@ -2416,20 +2444,50 @@ msgstr "فثشو يثسذقهحفهخر ذشررخف اشدث وخقث فاشر msgid "You are not currently a member of any team." msgstr "غخع شقث رخف ذعققثرفمغ ش وثوزثق خب شرغ فثشو." +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "شري خفاثقس" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "مشسف شذفهدهفغ %(date)s" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "دهثص %(span_start)s %(team_name)s %(span_end)s" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "شر ثققخق خذذعققثي. فقغ شلشهر." -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "مثشدث فاهس فثشو?" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" +"هب غخع مثشدث, غخع ذشر رخ مخرلثق حخسف هر فاهس فثشو'س يهسذعسسهخرس. غخعق حمشذث " +"صهمم زث شدشهمشزمث فخ شرخفاثق مثشقرثق." + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "ذخربهقو" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "غخع شمقثشيغ زثمخرل فخ شرخفاثق فثشو." -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "فاهس فثشو هس بعمم." @@ -2447,6 +2505,10 @@ msgstr "شمم فثشوس" msgid "teams" msgstr "فثشوس" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Teams" +msgstr "فثشوس" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "" "See all teams in your course, organized by topic. Join a team to collaborate" @@ -2455,33 +2517,56 @@ msgstr "" "سثث شمم فثشوس هر غخعق ذخعقسث, خقلشرهظثي زغ فخحهذ. تخهر ش فثشو فخ ذخممشزخقشفث" " صهفا خفاثق مثشقرثقس صاخ شقث هرفثقثسفثي هر فاث سشوث فخحهذ شس غخع شقث." -#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" -msgstr "فثشوس" - #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "My Team" msgstr "وغ فثشو" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "زقخصسث %(sr_start)s فثشوس %(sr_end)s" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." -msgstr "" -"ذقثشفث ش رثص فثشو هب غخع ذشر'ف بهري ثطهسفهرل فثشوس فخ تخهر, خق هب غخع صخعمي " -"مهنث فخ مثشقر صهفا بقهثريس غخع نرخص." +msgid "Team Search" +msgstr "فثشو سثشقذا" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" +msgstr "ساخصهرل قثسعمفس بخق \"%(searchString)s\"" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "ذقثشفث ش رثص فثشو" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" +"ذقثشفث ش رثص فثشو هب غخع ذشر'ف بهري شر ثطهسفهرل فثشو فخ تخهر, خق هب غخع " +"صخعمي مهنث فخ مثشقر صهفا بقهثريس غخع نرخص." + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "ثيهف فثشو" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" +"هب غخع وشنث سهلرهبهذشرف ذاشرلثس, وشنث سعقث غخع رخفهبغ وثوزثقس خب فاث فثشو " +"زثبخقث وشنهرل فاثسث ذاشرلثس." + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "سثشقذا فثشوس" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "شمم فخحهذس" @@ -2504,20 +2589,32 @@ msgid_plural "%(team_count)s Teams" msgstr[0] "%(team_count)s فثشو" msgstr[1] "%(team_count)s فثشوس" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "فخحهذ" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "دهثص فثشوس هر فاث %(topic_name)s فخحهذ" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" -"فقغ {browse_span_start}زقخصسهرل شمم فثشوس{span_end} خق " -"{search_span_start}سثشقذاهرل فثشو يثسذقهحفهخرس{span_end}. هب غخع سفهمم ذشر'ف" -" بهري ش فثشو فخ تخهر, {create_span_start}ذقثشفث ش رثص فثشو هر فاهس " +"{browse_span_start}زقخصسث فثشوس هر خفاثق فخحهذس{span_end} خق " +"{search_span_start}سثشقذا فثشوس{span_end} هر فاهس فخحهذ. هب غخع سفهمم ذشر'ف " +"بهري ش فثشو فخ تخهر, {create_span_start}ذقثشفث ش رثص فثشو هر فاهس " "فخحهذ{span_end}." #: lms/djangoapps/teams/static/teams/js/views/topics.js @@ -3955,11 +4052,6 @@ msgstr "فشنث ش حاخفخ خب غخعق هي" msgid "Review your info" msgstr "قثدهثص غخعق هربخ" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "ذخربهقو" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "شر ثققخق اشس خذذعققثي. حمثشسث فقغ قثمخشيهرل فاث حشلث." @@ -5144,18 +5236,6 @@ msgstr "" "فاث ذخوزهرثي مثرلفا خب فاث خقلشرهظشفهخر شري مهزقشقغ ذخيث بهثميس ذشررخف زث " "وخقث فاشر <%=limit%> ذاشقشذفثقس." -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "قثضعهقثي بهثمي." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "حمثشسث يخ رخف عسث شرغ سحشذثس هر فاهس بهثمي." - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "حمثشسث يخ رخف عسث شرغ سحشذثس خق سحثذهشم ذاشقشذفثقس هر فاهس بهثمي." - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "ذخوحخرثرف" @@ -5279,6 +5359,14 @@ msgstr "حشلث رعوزثق" msgid "Enter the page number you'd like to quickly navigate to." msgstr "ثرفثق فاث حشلث رعوزثق غخع'ي مهنث فخ ضعهذنمغ رشدهلشفث فخ." +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "سخقفثي زغ" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "ذمثشق سثشقذا" + #: common/static/common/templates/discussion/discussion-home.underscore msgid "DISCUSSION HOME:" msgstr "يهسذعسسهخر اخوث:" @@ -5682,8 +5770,12 @@ msgid "Regenerate the user's certificate" msgstr "قثلثرثقشفث فاث عسثق'س ذثقفهبهذشفث" #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "Your team could not be created!" -msgstr "غخعق فثشو ذخعمي رخف زث ذقثشفثي!" +msgid "Your team could not be created." +msgstr "غخعق فثشو ذخعمي رخف زث ذقثشفثي." + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "غخعق فثشو ذخعمي رخف زث عحيشفثي." #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore msgid "" @@ -5708,18 +5800,26 @@ msgstr "" "زث هرفثقثسفثي هر تخهرهرل غخعق فثشو هب هف سثثوس فخخ قثسفقهذفهدث." #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "{primaryButtonTitle} {span_start}a new team{span_end}" -msgstr "{primaryButtonTitle} {span_start}ش رثص فثشو{span_end}" +msgid "Create team." +msgstr "ذقثشفث فثشو." #: lms/djangoapps/teams/static/teams/templates/edit-team.underscore -msgid "Cancel {span_start}a new team{span_end}" -msgstr "ذشرذثم {span_start}ش رثص فثشو{span_end}" +msgid "Update team." +msgstr "عحيشفث فثشو." + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "ذشرذثم فثشو ذقثشفهرل." + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "ذشرذثم فثشو عحيشفهرل." #: lms/djangoapps/teams/static/teams/templates/team-actions.underscore msgid "Are you having trouble finding a team to join?" msgstr "شقث غخع اشدهرل فقخعزمث بهريهرل ش فثشو فخ تخهر?" -#: lms/djangoapps/teams/static/teams/templates/team-join.underscore +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore msgid "Join Team" msgstr "تخهر فثشو" @@ -6893,6 +6993,16 @@ msgstr "يقشل شري يقخح خق ذمهذن اثقث فخ عحمخشي ده msgid "status" msgstr "سفشفعس" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "شيي ذخوحخرثرف:" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "%(type)s ذخوحخرثرف فثوحمشفث وثرع" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "ذخووخر حقخزمثو فغحثس" @@ -6967,6 +7077,11 @@ msgstr "هي" msgid "Certificate Details" msgstr "ذثقفهبهذشفث يثفشهمس" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "ذخعقسث فهفمث" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -7013,19 +7128,13 @@ msgstr "" "ذثقفهبهذشفثس. مثشدث زمشرن فخ عسث فاث خببهذهشم ذخعقسث فهفمث." #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" -msgstr "شيي سهلرشفخقغ" +msgid "Add Additional Signatory" +msgstr "شيي شييهفهخرشم سهلرشفخقغ" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "(عح فخ 4 سهلرشفخقهثس شقث شممخصثي بخق ش ذثقفهبهذشفث)" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "ذقثشفث" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "ذاخخسث وخيث" diff --git a/conf/locale/ru/LC_MESSAGES/django.mo b/conf/locale/ru/LC_MESSAGES/django.mo index 91b0c8574c..89a2e224a7 100644 Binary files a/conf/locale/ru/LC_MESSAGES/django.mo and b/conf/locale/ru/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ru/LC_MESSAGES/django.po b/conf/locale/ru/LC_MESSAGES/django.po index 8755062b86..e87aecfe9c 100644 --- a/conf/locale/ru/LC_MESSAGES/django.po +++ b/conf/locale/ru/LC_MESSAGES/django.po @@ -180,7 +180,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-08-05 08:47+0000\n" "Last-Translator: Weyedide \n" "Language-Team: Russian (http://www.transifex.com/open-edx/edx-platform/language/ru/)\n" @@ -1341,10 +1341,6 @@ msgstr "" msgid "incorrect" msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "" @@ -1367,10 +1363,6 @@ msgstr "" msgid "This is incorrect." msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -4691,7 +4683,14 @@ msgid "{month} {day}, {year}" msgstr "{day} {month}, {year}" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -4791,13 +4790,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -4954,6 +4953,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5475,6 +5482,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -5854,6 +5878,10 @@ msgstr "" msgid "CourseMode price updated successfully" msgstr "" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "" @@ -5955,18 +5983,6 @@ msgstr "" msgid "Grades for assignment \"{name}\"" msgstr "" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "" @@ -6129,6 +6145,12 @@ msgstr "удалено" msgid "emailed" msgstr "" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6141,12 +6163,6 @@ msgstr "оценено" msgid "problem distribution graded" msgstr "" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7526,6 +7542,7 @@ msgid "Optional language the team uses as ISO 639-1 code." msgstr "" #: lms/djangoapps/teams/plugins.py +#: lms/djangoapps/teams/templates/teams/teams.html msgid "Teams" msgstr "" @@ -7538,11 +7555,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9240,6 +9257,10 @@ msgstr "Помощь" msgid "Sign Out" msgstr "" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "Все права защищены" @@ -11736,8 +11757,10 @@ msgid "Section:" msgstr "Раздел:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "Ссылка на задачу:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -13522,6 +13545,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -14310,7 +14347,7 @@ msgstr "Добавить модератора" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "Discussion Community TAs" -msgstr "" +msgstr "Старосты сообщества" #: lms/templates/instructor/instructor_dashboard_2/membership.html msgid "" @@ -15827,41 +15864,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html diff --git a/conf/locale/ru/LC_MESSAGES/djangojs.mo b/conf/locale/ru/LC_MESSAGES/djangojs.mo index 577a5d5b7b..1b659dfc52 100644 Binary files a/conf/locale/ru/LC_MESSAGES/djangojs.mo and b/conf/locale/ru/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/ru/LC_MESSAGES/djangojs.po b/conf/locale/ru/LC_MESSAGES/djangojs.po index f420177e64..017aec2adc 100644 --- a/conf/locale/ru/LC_MESSAGES/djangojs.po +++ b/conf/locale/ru/LC_MESSAGES/djangojs.po @@ -101,8 +101,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 02:41+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Russian (http://www.transifex.com/open-edx/edx-platform/language/ru/)\n" "MIME-Version: 1.0\n" @@ -148,8 +148,8 @@ msgstr "ОК" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -160,6 +160,11 @@ msgstr "ОК" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "Отмена" @@ -323,6 +328,8 @@ msgstr "" #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "" @@ -752,6 +759,7 @@ msgstr "Редактировать HTML" #: cms/templates/js/show-textbook.underscore #: cms/templates/js/signatory-details.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/forum-action-edit.underscore msgid "Edit" msgstr "" @@ -839,9 +847,11 @@ msgstr "" msgid "Formats" msgstr "" +#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/templates/image-modal.underscore msgid "Fullscreen" msgstr "" @@ -1157,6 +1167,8 @@ msgstr "Новое окно" #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore msgid "Next" msgstr "" @@ -1874,6 +1886,7 @@ msgstr "" #: common/static/coffee/src/discussion/utils.js #: common/static/coffee/src/discussion/views/discussion_thread_list_view.js #: common/static/coffee/src/discussion/views/discussion_topic_menu_view.js +#: common/static/common/templates/discussion/pagination.underscore msgid "…" msgstr "" @@ -2060,6 +2073,8 @@ msgid "Your post will be discarded." msgstr "" #: common/static/coffee/src/discussion/views/response_comment_show_view.js +#: common/static/common/templates/discussion/post-user-display.underscore +#: common/static/common/templates/discussion/profile-thread.underscore msgid "anonymous" msgstr "" @@ -2075,6 +2090,18 @@ msgstr "" msgid "Are you sure you want to delete this response?" msgstr "" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "" @@ -2256,6 +2283,7 @@ msgstr "Дата публикации" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2274,6 +2302,8 @@ msgid "Public" msgstr "" #: common/static/js/vendor/ova/catch/js/catch.js +#: common/static/common/templates/components/search-field.underscore +#: lms/djangoapps/support/static/support/templates/certificates.underscore msgid "Search" msgstr "" @@ -2335,13 +2365,16 @@ msgid "An unexpected error occurred. Please try again." msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -#: lms/djangoapps/teams/static/teams/js/collections/topic.js -#: lms/templates/edxnotes/tab-item.underscore -msgid "name" +msgid "last activity" msgstr "" #: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" +msgid "open slots" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/topic.js +#: lms/templates/edxnotes/tab-item.underscore +msgid "name" msgstr "" #. Translators: This refers to the number of teams (a count of how many teams @@ -2350,6 +2383,17 @@ msgstr "" msgid "team count" msgstr "количество команд" +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2374,6 +2418,7 @@ msgid "Language" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2384,6 +2429,7 @@ msgid "Country" msgstr "Страна" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2416,20 +2462,48 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/static/js/verify_student/views/reverify_view.js +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2450,13 +2524,13 @@ msgid "teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"See all teams in your course, organized by topic. Join a team to collaborate" -" with other learners who are interested in the same topic as you are." +msgid "Teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" +msgid "" +"See all teams in your course, organized by topic. Join a team to collaborate" +" with other learners who are interested in the same topic as you are." msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -2464,22 +2538,47 @@ msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2504,15 +2603,27 @@ msgstr[1] "" msgstr[2] "" msgstr[3] "" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -3862,11 +3973,6 @@ msgstr "" msgid "Review your info" msgstr "" -#: lms/static/js/verify_student/views/reverify_view.js -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "" @@ -4241,6 +4347,7 @@ msgstr "" #: cms/static/js/factories/manage_users.js #: cms/static/js/factories/manage_users_lib.js +#: common/static/common/templates/discussion/post-user-display.underscore msgid "Staff" msgstr "" @@ -4422,9 +4529,10 @@ msgstr "" msgid "Date Added" msgstr "" -#: cms/templates/js/asset-library.underscore +#: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore msgid "Type" -msgstr "Тип" +msgstr "" #: cms/static/js/views/assets.js msgid "File {filename} exceeds maximum size of {maxFileSizeInMBs} MB" @@ -4983,18 +5091,6 @@ msgid "" "more than <%=limit%> characters." msgstr "" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "" @@ -5084,11 +5180,526 @@ msgstr "" msgid "Due Date" msgstr "" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5129,12 +5740,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5224,10 +5829,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "ПОДРОБНЕЕ" @@ -6148,6 +6749,16 @@ msgstr "" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "Часто встречающиеся типы заданий" @@ -6222,6 +6833,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6266,19 +6882,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6706,10 +7316,6 @@ msgstr "" msgid "Add your first textbook" msgstr "" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "" diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.mo b/conf/locale/zh_CN/LC_MESSAGES/django.mo index ac803d8b9d..16231f892d 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/django.mo and b/conf/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.po b/conf/locale/zh_CN/LC_MESSAGES/django.po index 76f8d184ec..fc8a5e4748 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/django.po +++ b/conf/locale/zh_CN/LC_MESSAGES/django.po @@ -263,7 +263,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:18+0000\n" +"POT-Creation-Date: 2015-09-04 14:07+0000\n" "PO-Revision-Date: 2015-06-18 03:04+0000\n" "Last-Translator: louyihua \n" "Language-Team: Chinese (China) (http://www.transifex.com/open-edx/edx-platform/language/zh_CN/)\n" @@ -1400,10 +1400,6 @@ msgstr "正确" msgid "incorrect" msgstr "不正确" -#: common/lib/capa/capa/inputtypes.py -msgid "partially correct" -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "incomplete" msgstr "不完整" @@ -1426,10 +1422,6 @@ msgstr "" msgid "This is incorrect." msgstr "" -#: common/lib/capa/capa/inputtypes.py -msgid "This is partially correct." -msgstr "" - #: common/lib/capa/capa/inputtypes.py msgid "This is unanswered." msgstr "" @@ -4732,7 +4724,14 @@ msgid "{month} {day}, {year}" msgstr "" #: lms/djangoapps/certificates/views/webview.py -msgid "a course of study offered by {partner_name}, through {platform_name}." +msgid "" +"a course of study offered by {partner_short_name}, an online learning " +"initiative of {partner_long_name} through {platform_name}." +msgstr "" + +#: lms/djangoapps/certificates/views/webview.py +msgid "" +"a course of study offered by {partner_short_name}, through {platform_name}." msgstr "" #. Translators: Accomplishments describe the awards/certifications obtained by @@ -4832,13 +4831,13 @@ msgstr "" #: lms/djangoapps/certificates/views/webview.py msgid "" "This is a valid {platform_name} certificate for {user_name}, who " -"participated in {partner_name} {course_number}" +"participated in {partner_short_name} {course_number}" msgstr "" #. Translators: This text is bound to the HTML 'title' element of the page #. and appears in the browser title bar #: lms/djangoapps/certificates/views/webview.py -msgid "{partner_name} {course_number} Certificate | {platform_name}" +msgid "{partner_short_name} {course_number} Certificate | {platform_name}" msgstr "" #. Translators: This text fragment appears after the student's name @@ -4992,6 +4991,14 @@ msgid "" "{payment_support_link}." msgstr "" +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "{course_id} is not a valid course key." +msgstr "" + +#: lms/djangoapps/commerce/api/v1/serializers.py +msgid "Course {course_id} does not exist." +msgstr "" + #: lms/djangoapps/course_wiki/tab.py lms/djangoapps/course_wiki/views.py #: lms/templates/wiki/base.html msgid "Wiki" @@ -5512,6 +5519,23 @@ msgstr "" msgid "File is not attached." msgstr "" +#: lms/djangoapps/instructor/views/api.py +msgid "Could not find problem with this location." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"The problem responses report is being created. To view the status of the " +"report, see Pending Tasks below." +msgstr "" + +#: lms/djangoapps/instructor/views/api.py +msgid "" +"A problem responses report generation task is already in progress. Check the" +" 'Pending Tasks' table for the status of the task. When completed, the " +"report will be available for download in the table below." +msgstr "" + #: lms/djangoapps/instructor/views/api.py msgid "Invoice number '{num}' does not exist." msgstr "" @@ -5898,6 +5922,10 @@ msgstr "" msgid "CourseMode price updated successfully" msgstr "" +#: lms/djangoapps/instructor/views/instructor_dashboard.py +msgid "No end date set" +msgstr "" + #: lms/djangoapps/instructor/views/instructor_dashboard.py msgid "Enrollment data is now available in {dashboard_link}." msgstr "选课数据现已可通过{dashboard_link}查看。" @@ -5991,18 +6019,6 @@ msgstr "外部邮箱" msgid "Grades for assignment \"{name}\"" msgstr " 作业“{name}”的成绩" -#: lms/djangoapps/instructor/views/legacy.py -msgid "Found {num} records to dump." -msgstr "共有 {num} 条记录需要转储" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Couldn't find module with that urlname." -msgstr "无法找到该地址对应的模块" - -#: lms/djangoapps/instructor/views/legacy.py -msgid "Student state for problem {problem}" -msgstr "{problem}问答的学生状态" - #: lms/djangoapps/instructor/views/legacy.py msgid "Grades from {course_id}" msgstr "课程“{course_id}”的成绩" @@ -6166,6 +6182,12 @@ msgstr "已删除" msgid "emailed" msgstr "已邮件通知" +#. Translators: This is a past-tense verb that is inserted into task progress +#. messages as {action}. +#: lms/djangoapps/instructor_task/tasks.py +msgid "generated" +msgstr "" + #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -6178,12 +6200,6 @@ msgstr "已评价" msgid "problem distribution graded" msgstr "" -#. Translators: This is a past-tense verb that is inserted into task progress -#. messages as {action}. -#: lms/djangoapps/instructor_task/tasks.py -msgid "generated" -msgstr "" - #. Translators: This is a past-tense verb that is inserted into task progress #. messages as {action}. #: lms/djangoapps/instructor_task/tasks.py @@ -7611,6 +7627,7 @@ msgid "Optional language the team uses as ISO 639-1 code." msgstr "" #: lms/djangoapps/teams/plugins.py +#: lms/djangoapps/teams/templates/teams/teams.html msgid "Teams" msgstr "" @@ -7623,11 +7640,11 @@ msgid "course_id must be provided" msgstr "" #: lms/djangoapps/teams/views.py -msgid "The supplied topic id {topic_id} is not valid" +msgid "text_search and order_by cannot be provided together" msgstr "" #: lms/djangoapps/teams/views.py -msgid "text_search is not yet supported." +msgid "The supplied topic id {topic_id} is not valid" msgstr "" #. Translators: 'ordering' is a string describing a way @@ -9347,6 +9364,10 @@ msgstr "帮助" msgid "Sign Out" msgstr "退出" +#: common/lib/capa/capa/templates/codeinput.html +msgid "{programming_language} editor" +msgstr "" + #: common/templates/license.html msgid "All Rights Reserved" msgstr "保留所有权利" @@ -11849,8 +11870,10 @@ msgid "Section:" msgstr "章:" #: lms/templates/courseware/legacy_instructor_dashboard.html -msgid "Problem urlname:" -msgstr "问题的URL地址:" +msgid "" +"To download a CSV listing student responses to a given problem, visit the " +"Data Download section of the Instructor Dashboard." +msgstr "" #: lms/templates/courseware/legacy_instructor_dashboard.html msgid "" @@ -13640,6 +13663,20 @@ msgstr "" msgid "Generate Proctored Exam Results Report" msgstr "" +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "" +"To generate a CSV file that lists all student answers to a given problem, " +"enter the location of the problem (from its Staff Debug Info)." +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Problem location: " +msgstr "" + +#: lms/templates/instructor/instructor_dashboard_2/data_download.html +msgid "Download a CSV of problem responses" +msgstr "" + #: lms/templates/instructor/instructor_dashboard_2/data_download.html msgid "" "For smaller courses, click to list profile information for enrolled students" @@ -15942,41 +15979,50 @@ msgid "This module is not enabled." msgstr "" #: cms/templates/certificates.html -msgid "" -"Upon successful completion of your course, learners receive a certificate to" -" acknowledge their accomplishment. If you are a course team member with the " -"Admin role in Studio, you can configure your course certificate." +msgid "Working with Certificates" msgstr "" #: cms/templates/certificates.html msgid "" -"Click {em_start}Add your first certificate{em_end} to add a certificate " -"configuration. Upload the organization logo to be used on the certificate, " -"and specify at least one signatory. You can include up to four signatories " -"for a certificate. You can also upload a signature image file for each " -"signatory. {em_start}Note:{em_end} Signature images are used only for " -"verified certificates. Optionally, specify a different course title to use " -"on your course certificate. You might want to use a different title if, for " -"example, the official course name is too long to display well on a " -"certificate." +"Specify a course title to use on the certificate if the course's official " +"title is too long to be displayed well." msgstr "" #: cms/templates/certificates.html msgid "" -"Select a course mode and click {em_start}Preview Certificate{em_end} to " -"preview the certificate that a learner in the selected enrollment track " -"would receive. When the certificate is ready for issuing, click " -"{em_start}Activate.{em_end} To stop issuing an active certificate, click " -"{em_start}Deactivate{em_end}." +"For verified certificates, specify between one and four signatories and " +"upload the associated images." msgstr "" #: cms/templates/certificates.html msgid "" -" To edit the certificate configuration, hover over the top right corner of " -"the form and click {em_start}Edit{em_end}. To delete a certificate, hover " -"over the top right corner of the form and click the delete icon. In general," -" do not delete certificates after a course has started, because some " -"certificates might already have been issued to learners." +"To edit or delete a certificate before it is activated, hover over the top " +"right corner of the form and select {em_start}Edit{em_end} or the delete " +"icon." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To view a sample certificate, choose a course mode and select " +"{em_start}Preview Certificate{em_end}." +msgstr "" + +#: cms/templates/certificates.html +msgid "Issuing Certificates to Learners" +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"To begin issuing certificates, a course team member with the Admin role " +"selects {em_start}Activate{em_end}. Course team members without the Admin " +"role cannot edit or delete an activated certificate." +msgstr "" + +#: cms/templates/certificates.html +msgid "" +"{em_start}Do not{em_end} delete certificates after a course has started; " +"learners who have already earned certificates will no longer be able to " +"access them." msgstr "" #: cms/templates/certificates.html diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo index 858d4c7b16..6e2f01139d 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo and b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po index f15920ca35..21ab1fa3b5 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po +++ b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po @@ -94,6 +94,7 @@ # This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE. # # Translators: +# Bill , 2015 # Changyue Wang , 2015 # CharlotteDing , 2015 # jsgang , 2014-2015 @@ -127,8 +128,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2015-08-21 14:17+0000\n" -"PO-Revision-Date: 2015-08-21 12:22+0000\n" +"POT-Creation-Date: 2015-09-04 14:06+0000\n" +"PO-Revision-Date: 2015-09-04 14:08+0000\n" "Last-Translator: Sarina Canelake \n" "Language-Team: Chinese (China) (http://www.transifex.com/open-edx/edx-platform/language/zh_CN/)\n" "MIME-Version: 1.0\n" @@ -174,8 +175,8 @@ msgstr "是的" #: cms/static/js/views/show_textbook.js cms/static/js/views/validation.js #: cms/static/js/views/modals/base_modal.js #: cms/static/js/views/modals/course_outline_modals.js -#: cms/static/js/views/utils/view_utils.js #: common/lib/xmodule/xmodule/js/src/html/edit.js +#: common/static/common/js/components/utils/view_utils.js #: cms/templates/js/add-xblock-component-menu-problem.underscore #: cms/templates/js/add-xblock-component-menu.underscore #: cms/templates/js/certificate-editor.underscore @@ -186,6 +187,11 @@ msgstr "是的" #: cms/templates/js/group-configuration-editor.underscore #: cms/templates/js/section-name-edit.underscore #: cms/templates/js/xblock-string-field-editor.underscore +#: common/static/common/templates/discussion/new-post.underscore +#: common/static/common/templates/discussion/response-comment-edit.underscore +#: common/static/common/templates/discussion/thread-edit.underscore +#: common/static/common/templates/discussion/thread-response-edit.underscore +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore #: lms/templates/instructor/instructor_dashboard_2/cohort-form.underscore msgid "Cancel" msgstr "取消" @@ -195,17 +201,6 @@ msgstr "取消" #: cms/static/js/views/manage_users_and_roles.js #: cms/static/js/views/show_textbook.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/course-outline.underscore -#: cms/templates/js/course_grade_policy.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/group-configuration-editor.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-editor.underscore -#: cms/templates/js/xblock-outline.underscore msgid "Delete" msgstr "删除" @@ -371,6 +366,8 @@ msgstr "您的成绩达不到要求,不能进行下一步骤。" #: common/lib/xmodule/xmodule/js/src/combinedopenended/display.js #: lms/static/coffee/src/staff_grading/staff_grading.js +#: common/static/common/templates/discussion/thread-response.underscore +#: common/static/common/templates/discussion/thread.underscore #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" msgstr "提交" @@ -791,18 +788,10 @@ msgstr "文档属性" msgid "Edit HTML" msgstr "编辑 HTML" -#. #-#-#-#-# djangojs-partial.po (edx-platform) #-#-#-#-# #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js #: common/static/js/vendor/ova/catch/js/catch.js -#: cms/templates/js/certificate-details.underscore -#: cms/templates/js/content-group-details.underscore -#: cms/templates/js/course_info_handouts.underscore -#: cms/templates/js/group-configuration-details.underscore -#: cms/templates/js/show-textbook.underscore -#: cms/templates/js/signatory-details.underscore -#: cms/templates/js/xblock-string-field-editor.underscore msgid "Edit" msgstr "编辑" @@ -2107,6 +2096,18 @@ msgstr "在删除这条评论时出错,请再试一遍。" msgid "Are you sure you want to delete this response?" msgstr "你确定要删除这个回复吗" +#: common/static/common/js/components/utils/view_utils.js +msgid "Required field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces in this field." +msgstr "" + +#: common/static/common/js/components/utils/view_utils.js +msgid "Please do not use any spaces or special characters in this field." +msgstr "" + #: common/static/common/js/components/views/paging_header.js msgid "Showing %(first_index)s out of %(num_items)s total" msgstr "" @@ -2264,6 +2265,7 @@ msgstr "发表日期" #: common/static/js/vendor/ova/catch/js/catch.js #: lms/static/js/courseware/credit_progress.js +#: common/static/common/templates/discussion/forum-actions.underscore #: lms/templates/discovery/facet.underscore #: lms/templates/edxnotes/note-item.underscore msgid "More" @@ -2341,20 +2343,34 @@ msgstr "标签:" msgid "An unexpected error occurred. Please try again." msgstr "" +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "last activity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/collections/team.js +msgid "open slots" +msgstr "" + #: lms/templates/edxnotes/tab-item.underscore msgid "name" msgstr "名称" -#: lms/djangoapps/teams/static/teams/js/collections/team.js -msgid "open_slots" -msgstr "" - #. Translators: This refers to the number of teams (a count of how many teams #. there are) #: lms/djangoapps/teams/static/teams/js/collections/topic.js msgid "team count" msgstr "" +#: cms/templates/js/certificate-editor.underscore +#: cms/templates/js/content-group-editor.underscore +#: cms/templates/js/group-configuration-editor.underscore +msgid "Create" +msgstr "创建" + +#: lms/djangoapps/teams/static/teams/js/views/edit_team.js +msgid "Update" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/edit_team.js msgid "Team Name (Required) *" msgstr "" @@ -2379,6 +2395,7 @@ msgid "Language" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "" "The language that team members primarily use to communicate with each other." msgstr "" @@ -2389,6 +2406,7 @@ msgid "Country" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/edit_team.js +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore msgid "The country that team members primarily identify with." msgstr "" @@ -2421,20 +2439,46 @@ msgstr "" msgid "You are not currently a member of any team." msgstr "" +#. Translators: "and others" refers to fact that additional members of a team +#. exist that are not displayed. +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "and others" +msgstr "" + +#. Translators: 'date' is a placeholder for a fuzzy, relative timestamp (see: +#. https://github.com/rmm5t/jquery-timeago) +#: lms/djangoapps/teams/static/teams/js/views/team_card.js +msgid "Last Activity %(date)s" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/team_card.js msgid "View %(span_start)s %(team_name)s %(span_end)s" msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js #: lms/djangoapps/teams/static/teams/js/views/team_profile.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "An error occurred. Try again." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "Leave this team?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile.js +msgid "" +"If you leave, you can no longer post in this team's discussions. Your place " +"will be available to another learner." +msgstr "" + +#: lms/templates/verify_student/review_photos_step.underscore +msgid "Confirm" +msgstr "确认" + +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "You already belong to another team." msgstr "" -#: lms/djangoapps/teams/static/teams/js/views/team_join.js +#: lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js msgid "This team is full." msgstr "" @@ -2452,13 +2496,13 @@ msgid "teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"See all teams in your course, organized by topic. Join a team to collaborate" -" with other learners who are interested in the same topic as you are." +msgid "Teams" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "Teams" +msgid "" +"See all teams in your course, organized by topic. Join a team to collaborate" +" with other learners who are interested in the same topic as you are." msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -2466,22 +2510,47 @@ msgid "My Team" msgstr "" #. Translators: sr_start and sr_end surround text meant only for screen -#. readers. The whole string will be shown to users as "Browse teams" if they -#. are using a screenreader, and "Browse" otherwise. +#. readers. +#. The whole string will be shown to users as "Browse teams" if they are using +#. a +#. screenreader, and "Browse" otherwise. #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Browse %(sr_start)s teams %(sr_end)s" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js -msgid "" -"Create a new team if you can't find existing teams to join, or if you would " -"like to learn with friends you know." +msgid "Team Search" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Showing results for \"%(searchString)s\"" msgstr "" #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "Create a New Team" msgstr "" +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"Create a new team if you can't find an existing team to join, or if you " +"would like to learn with friends you know." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Edit Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "" +"If you make significant changes, make sure you notify members of the team " +"before making these changes." +msgstr "" + +#: lms/djangoapps/teams/static/teams/js/views/teams_tab.js +msgid "Search teams" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/teams_tab.js msgid "All Topics" msgstr "" @@ -2503,15 +2572,27 @@ msgid "%(team_count)s Team" msgid_plural "%(team_count)s Teams" msgstr[0] "" +#: lms/djangoapps/teams/static/teams/js/views/topic_card.js +msgid "Topic" +msgstr "" + #: lms/djangoapps/teams/static/teams/js/views/topic_card.js msgid "View Teams in the %(topic_name)s Topic" msgstr "" +#. Translators: this string is shown at the bottom of the teams page +#. to find a team to join or else to create a new one. There are three +#. links that need to be included in the message: +#. 1. Browse teams in other topics +#. 2. search teams +#. 3. create a new team +#. Be careful to start each link with the appropriate start indicator +#. (e.g. {browse_span_start} for #1) and finish it with {span_end}. #: lms/djangoapps/teams/static/teams/js/views/topic_teams.js msgid "" -"Try {browse_span_start}browsing all teams{span_end} or " -"{search_span_start}searching team descriptions{span_end}. If you still can't" -" find a team to join, {create_span_start}create a new team in this " +"{browse_span_start}Browse teams in other topics{span_end} or " +"{search_span_start}search teams{span_end} in this topic. If you still can't " +"find a team to join, {create_span_start}create a new team in this " "topic{span_end}." msgstr "" @@ -3851,10 +3932,6 @@ msgstr "" msgid "Review your info" msgstr "" -#: lms/templates/verify_student/review_photos_step.underscore -msgid "Confirm" -msgstr "确认" - #: lms/static/js/verify_student/views/step_view.js msgid "An error has occurred. Please try reloading the page." msgstr "发生了一个错误。请重新加载这个页面。" @@ -4397,7 +4474,7 @@ msgstr "您的文件已经被删除" msgid "Date Added" msgstr "添加日期" -#: cms/static/js/views/assets.js cms/templates/js/asset-library.underscore +#: cms/static/js/views/assets.js msgid "Type" msgstr "类型" @@ -4952,18 +5029,6 @@ msgid "" "more than <%=limit%> characters." msgstr "机构和知识库编号字段合在一起不能超过 <%=limit%> 个字符" -#: cms/static/js/views/utils/view_utils.js -msgid "Required field." -msgstr "必须填写的字段。" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces in this field." -msgstr "此字段的内容不能包含空格。" - -#: cms/static/js/views/utils/view_utils.js -msgid "Please do not use any spaces or special characters in this field." -msgstr "此字段的内容不能包含空格或特殊字符。" - #: cms/static/js/views/utils/xblock_utils.js msgid "component" msgstr "组件" @@ -5053,11 +5118,526 @@ msgstr "" msgid "Due Date" msgstr "截止日期" +#: cms/templates/js/paging-header.underscore +#: common/static/common/templates/components/paging-footer.underscore +#: common/static/common/templates/discussion/pagination.underscore +msgid "Previous" +msgstr "" + #: cms/templates/js/previous-video-upload-list.underscore +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore #: lms/templates/verify_student/enrollment_confirmation_step.underscore msgid "Status" msgstr "状态" +#: common/static/common/templates/image-modal.underscore +msgid "Large" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom In" +msgstr "" + +#: common/static/common/templates/image-modal.underscore +msgid "Zoom Out" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Page number" +msgstr "" + +#: common/static/common/templates/components/paging-footer.underscore +msgid "Enter the page number you'd like to quickly navigate to." +msgstr "" + +#: common/static/common/templates/components/paging-header.underscore +msgid "Sorted by" +msgstr "" + +#: common/static/common/templates/components/search-field.underscore +msgid "Clear search" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "DISCUSSION HOME:" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +#: lms/templates/commerce/provider.underscore +#: lms/templates/commerce/receipt.underscore +#: lms/templates/discovery/course_card.underscore +msgid "gettext(" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Find discussions" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Focus in on specific topics" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Search for specific posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Sort by date, vote, or comments" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Engage with posts" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Upvote posts and good responses" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Report Forum Misuse" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Follow posts for updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Receive updates" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "Toggle Notifications Setting" +msgstr "" + +#: common/static/common/templates/discussion/discussion-home.underscore +msgid "" +"Check this box to receive an email digest once a day notifying you about " +"new, unread activity from posts you are following." +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Mark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-answer.underscore +msgid "Unmark as Answer" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-close.underscore +msgid "Open" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Endorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-endorse.underscore +msgid "Unendorse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Follow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-follow.underscore +msgid "Unfollow" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Pin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-pin.underscore +msgid "Unpin" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report abuse" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Report" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-report.underscore +msgid "Unreport" +msgstr "" + +#: common/static/common/templates/discussion/forum-action-vote.underscore +msgid "Vote for this post," +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Visible To:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "All Groups" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "" +"Discussion admins, moderators, and TAs can make their posts visible to all " +"students or specify a single cohort." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Title:" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add a clear and descriptive title to encourage participation." +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Enter your question or comment" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "follow this post" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "post anonymously to classmates" +msgstr "" + +#: common/static/common/templates/discussion/new-post.underscore +msgid "Add Post" +msgstr "" + +#: common/static/common/templates/discussion/post-user-display.underscore +msgid "Community TA" +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +#: common/static/common/templates/discussion/thread.underscore +msgid "This thread is closed." +msgstr "" + +#: common/static/common/templates/discussion/profile-thread.underscore +msgid "View discussion" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Editing comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-edit.underscore +msgid "Update comment" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#, python-format +msgid "posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/response-comment-show.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Reported" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Editing post" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Edit post title" +msgstr "" + +#: common/static/common/templates/discussion/thread-edit.underscore +msgid "Update post" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "answered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "unanswered question" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Pinned" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "Following" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Staff" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +msgid "By: Community TA" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#: common/static/common/templates/discussion/thread-response-show.underscore +msgid "fmt" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "" +"%(comments_count)s %(span_sr_open)scomments (%(unread_comments_count)s " +"unread comments)%(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-list-item.underscore +#, python-format +msgid "%(comments_count)s %(span_sr_open)scomments %(span_close)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Editing response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-edit.underscore +msgid "Update response" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "marked as answer %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s by %(user)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response-show.underscore +#, python-format +msgid "endorsed %(time_ago)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "fmts" +msgstr "" + +#: common/static/common/templates/discussion/thread-response.underscore +msgid "Add a comment" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "This post is visible only to %(group_name)s." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "This post is visible to everyone." +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "%(post_type)s posted %(time_ago)s by %(author)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +msgid "Closed" +msgstr "" + +#: common/static/common/templates/discussion/thread-show.underscore +#, python-format +msgid "Related to: %(courseware_title_linked)s" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Post type:" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Question" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "Discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread-type.underscore +msgid "" +"Questions raise issues that need answers. Discussions share ideas and start " +"conversations." +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Add a Response" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Post a response:" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Expand discussion" +msgstr "" + +#: common/static/common/templates/discussion/thread.underscore +msgid "Collapse discussion" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Topic Area:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Discussion topics; current selection is:" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Filter topics" +msgstr "" + +#: common/static/common/templates/discussion/topic.underscore +msgid "Add your post to a relevant topic to help others find it." +msgstr "" + +#: common/static/common/templates/discussion/user-profile.underscore +msgid "Active Threads" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates.underscore +msgid "username or email" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "No results" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Course Key" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download URL" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Grade" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Last Updated" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Download the user's certificate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Not available" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate" +msgstr "" + +#: lms/djangoapps/support/static/support/templates/certificates_results.underscore +msgid "Regenerate the user's certificate" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be created." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Your team could not be updated." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Enter information to describe your team. You cannot change these details " +"after you create the team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Optional Characteristics" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "" +"Help other learners decide whether to join your team by specifying some " +"characteristics for your team. Choose carefully, because fewer people might " +"be interested in joining your team if it seems too restrictive." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Create team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Update team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team creating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/edit-team.underscore +msgid "Cancel team updating." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-actions.underscore +msgid "Are you having trouble finding a team to join?" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile-header-actions.underscore +msgid "Join Team" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "New Post" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team Details" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "You are a member of this team." +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team member profiles" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Team capacity" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "country" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "language" +msgstr "" + +#: lms/djangoapps/teams/static/teams/templates/team-profile.underscore +msgid "Leave Team" +msgstr "" + +#: lms/static/js/fixtures/donation.underscore +#: lms/templates/dashboard/donation.underscore +msgid "Donate" +msgstr "捐助" + #: lms/templates/ccx/schedule.underscore msgid "Expand All" msgstr "" @@ -5098,12 +5678,6 @@ msgstr "" msgid "Subsection" msgstr "" -#: lms/templates/commerce/provider.underscore -#: lms/templates/commerce/receipt.underscore -#: lms/templates/discovery/course_card.underscore -msgid "gettext(" -msgstr "" - #: lms/templates/commerce/provider.underscore #, python-format msgid "%s" @@ -5193,10 +5767,6 @@ msgstr "" msgid "End My Exam" msgstr "" -#: lms/templates/dashboard/donation.underscore -msgid "Donate" -msgstr "捐助" - #: lms/templates/discovery/course_card.underscore msgid "LEARN MORE" msgstr "" @@ -6115,6 +6685,16 @@ msgstr "拖放文件至此处或点击这里来上传视频文件。" msgid "status" msgstr "" +#: cms/templates/js/add-xblock-component-button.underscore +msgid "Add Component:" +msgstr "" + +#: cms/templates/js/add-xblock-component-menu-problem.underscore +#: cms/templates/js/add-xblock-component-menu.underscore +#, python-format +msgid "%(type)s Component Template Menu" +msgstr "" + #: cms/templates/js/add-xblock-component-menu-problem.underscore msgid "Common Problem Types" msgstr "常见问题类型" @@ -6189,6 +6769,11 @@ msgstr "" msgid "Certificate Details" msgstr "" +#: cms/templates/js/certificate-details.underscore +#: cms/templates/js/certificate-editor.underscore +msgid "Course Title" +msgstr "" + #: cms/templates/js/certificate-details.underscore #: cms/templates/js/certificate-editor.underscore msgid "Course Title Override" @@ -6233,19 +6818,13 @@ msgid "" msgstr "" #: cms/templates/js/certificate-editor.underscore -msgid "Add Signatory" +msgid "Add Additional Signatory" msgstr "" #: cms/templates/js/certificate-editor.underscore msgid "(Up to 4 signatories are allowed for a certificate)" msgstr "" -#: cms/templates/js/certificate-editor.underscore -#: cms/templates/js/content-group-editor.underscore -#: cms/templates/js/group-configuration-editor.underscore -msgid "Create" -msgstr "创建" - #: cms/templates/js/certificate-web-preview.underscore msgid "Choose mode" msgstr "" @@ -6670,10 +7249,6 @@ msgstr "您尚未向该课程添加任何课本。" msgid "Add your first textbook" msgstr "添加第一本课本" -#: cms/templates/js/paging-header.underscore -msgid "Previous" -msgstr "" - #: cms/templates/js/previous-video-upload-list.underscore msgid "Previous Uploads" msgstr "过去上传的文件" diff --git a/docs/shared/requirements.txt b/docs/shared/requirements.txt index bbda26ef24..48bfee0c39 100644 --- a/docs/shared/requirements.txt +++ b/docs/shared/requirements.txt @@ -53,7 +53,7 @@ pytz==2015.2 PyYAML==3.10 requests==2.3.0 Shapely==1.2.16 -sorl-thumbnail==11.12 +sorl-thumbnail==12.3 South==1.0.1 sympy==0.7.1 xmltodict==0.4.1 diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 21c8930d43..ea65a064f7 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -361,485 +361,6 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase): self.assertNotIn('This should not survive being overwritten by static content', response.content) -@attr('shard_1') -class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): - """ - Tests for the certificates web/html views - """ - def setUp(self): - super(CertificatesViewsTests, self).setUp() - self.client = Client() - self.course = CourseFactory.create( - org='testorg', number='run1', display_name='refundable course' - ) - self.course_id = self.course.location.course_key - self.user = UserFactory.create( - email='joe_user@edx.org', - username='joeuser', - password='foo' - ) - self.user.profile.name = "Joe User" - self.user.profile.save() - self.client.login(username=self.user.username, password='foo') - self.request = RequestFactory().request() - - self.cert = GeneratedCertificate.objects.create( - user=self.user, - course_id=self.course_id, - verify_uuid=uuid4(), - download_uuid=uuid4(), - download_url="http://www.example.com/certificates/download", - grade="0.95", - key='the_key', - distinction=True, - status='generated', - mode='honor', - name=self.user.profile.name, - ) - CourseEnrollmentFactory.create( - user=self.user, - course_id=self.course_id - ) - CertificateHtmlViewConfigurationFactory.create() - LinkedInAddToProfileConfigurationFactory.create() - - def _add_course_certificates(self, count=1, signatory_count=0, is_active=True): - """ - Create certificate for the course. - """ - signatories = [ - { - 'name': 'Signatory_Name ' + str(i), - 'title': 'Signatory_Title ' + str(i), - 'organization': 'Signatory_Organization ' + str(i), - 'signature_image_path': '/static/certificates/images/demo-sig{}.png'.format(i), - 'id': i, - } for i in xrange(0, signatory_count) - - ] - - certificates = [ - { - 'id': i, - 'name': 'Name ' + str(i), - 'description': 'Description ' + str(i), - 'course_title': 'course_title_' + str(i), - 'org_logo_path': '/t4x/orgX/testX/asset/org-logo-{}.png'.format(i), - 'signatories': signatories, - 'version': 1, - 'is_active': is_active - } for i in xrange(0, count) - ] - - self.course.certificates = {'certificates': certificates} - self.course.cert_html_view_enabled = True - self.course.save() - self.store.update_item(self.course, self.user.id) - - def _create_custom_template(self, org_id=None, mode=None, course_key=None): - """ - Creates a custom certificate template entry in DB. - """ - template_html = """ - - - lang: ${LANGUAGE_CODE} - course name: ${accomplishment_copy_course_name} - mode: ${course_mode} - ${accomplishment_copy_course_description} - - - """ - template = CertificateTemplate( - name='custom template', - template=template_html, - organization_id=org_id, - course_key=course_key, - mode=mode, - is_active=True - ) - template.save() - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_linkedin_share_url(self): - """ - Test: LinkedIn share URL. - """ - self._add_course_certificates(count=1, signatory_count=1, is_active=True) - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertTrue(urllib.quote_plus(self.request.build_absolute_uri(test_url)) in response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_rendering_course_organization_data(self): - """ - Test: organization data should render on certificate web view if course has organization. - """ - test_organization_data = { - 'name': 'test_organization', - 'description': 'Test Organization Description', - 'active': True, - 'logo': '/logo_test1.png/' - } - test_org = organizations_api.add_organization(organization_data=test_organization_data) - organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id)) - self._add_course_certificates(count=1, signatory_count=1, is_active=True) - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertIn( - 'a course of study offered by test_organization', - response.content - ) - self.assertNotIn( - 'a course of study offered by testorg', - response.content - ) - self.assertIn( - 'test_organization {} Certificate |'.format(self.course.number, ), - response.content - ) - self.assertIn('logo_test1.png', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_valid_certificate(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - self._add_course_certificates(count=1, signatory_count=2) - response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) - - # Hit any "verified" mode-specific branches - self.cert.mode = 'verified' - self.cert.save() - response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) - - # Hit any 'xseries' mode-specific branches - self.cert.mode = 'xseries' - self.cert.save() - response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_with_valid_signatories(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - self._add_course_certificates(count=1, signatory_count=2) - response = self.client.get(test_url) - self.assertIn('course_title_0', response.content) - self.assertIn('Signatory_Name 0', response.content) - self.assertIn('Signatory_Title 0', response.content) - self.assertIn('Signatory_Organization 0', response.content) - self.assertIn('/static/certificates/images/demo-sig0.png', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_course_display_name_not_override_with_course_title(self): - # if certificate in descriptor has not course_title then course name should not be overridden with this title. - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - test_certificates = [ - { - 'id': 0, - 'name': 'Name 0', - 'description': 'Description 0', - 'signatories': [], - 'version': 1, - 'is_active':True - } - ] - self.course.certificates = {'certificates': test_certificates} - self.course.cert_html_view_enabled = True - self.course.save() - self.store.update_item(self.course, self.user.id) - response = self.client.get(test_url) - self.assertNotIn('test_course_title_0', response.content) - self.assertIn('refundable course', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_certificate_view_without_org_logo(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - test_certificates = [ - { - 'id': 0, - 'name': 'Certificate Name 0', - 'signatories': [], - 'version': 1, - 'is_active': True - } - ] - self.course.certificates = {'certificates': test_certificates} - self.course.cert_html_view_enabled = True - self.course.save() - self.store.update_item(self.course, self.user.id) - response = self.client.get(test_url) - # make sure response html has only one organization logo container for edX - self.assertContains(response, "<li class=\"wrapper-organization\">", 1) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_without_signatories(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course) - ) - self._add_course_certificates(count=1, signatory_count=0) - response = self.client.get(test_url) - self.assertNotIn('Signatory_Name 0', response.content) - self.assertNotIn('Signatory_Title 0', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) - def test_render_html_view_disabled_feature_flag_returns_static_url(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - self.assertIn(str(self.cert.download_url), test_url) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_invalid_course_id(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id='az/23423/4vs' - ) - - response = self.client.get(test_url) - self.assertIn('invalid', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_invalid_course(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id='missing/course/key' - ) - response = self.client.get(test_url) - self.assertIn('invalid', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_invalid_user(self): - test_url = get_certificate_url( - user_id=111, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertIn('invalid', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_invalid_user_certificate(self): - self.cert.delete() - self.assertEqual(len(GeneratedCertificate.objects.all()), 0) - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertIn('invalid', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_with_preview_mode(self): - """ - test certificate web view should render properly along with its signatories information when accessing it in - preview mode. Either the certificate is marked active or not. - """ - self.cert.delete() - self.assertEqual(len(GeneratedCertificate.objects.all()), 0) - self._add_course_certificates(count=1, signatory_count=2) - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url + '?preview=honor') - self.assertNotIn(self.course.display_name, response.content) - self.assertIn('course_title_0', response.content) - self.assertIn('Signatory_Title 0', response.content) - - # mark certificate inactive but accessing in preview mode. - self._add_course_certificates(count=1, signatory_count=2, is_active=False) - response = self.client.get(test_url + '?preview=honor') - self.assertNotIn(self.course.display_name, response.content) - self.assertIn('course_title_0', response.content) - self.assertIn('Signatory_Title 0', response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_render_html_view_invalid_certificate_configuration(self): - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertIn("Invalid Certificate", response.content) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_certificate_evidence_event_emitted(self): - self.client.logout() - self._add_course_certificates(count=1, signatory_count=2) - self.recreate_tracker() - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - actual_event = self.get_event() - self.assertEqual(actual_event['name'], 'edx.certificate.evidence_visited') - assert_event_matches( - { - 'user_id': self.user.id, - 'certificate_id': unicode(self.cert.verify_uuid), - 'enrollment_mode': self.cert.mode, - 'certificate_url': test_url, - 'course_id': unicode(self.course.id), - 'social_network': CertificateSocialNetworks.linkedin - }, - actual_event['data'] - ) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_evidence_event_sent(self): - cert_url = get_certificate_url( - user_id=self.user.id, - course_id=self.course_id - ) - test_url = '{}?evidence_visit=1'.format(cert_url) - self._add_course_certificates(count=1, signatory_count=2) - self.recreate_tracker() - assertion = BadgeAssertion( - user=self.user, course_id=self.course_id, mode='honor', - data={ - 'image': 'http://www.example.com/image.png', - 'json': {'id': 'http://www.example.com/assertion.json'}, - 'issuer': 'http://www.example.com/issuer.json', - - } - ) - assertion.save() - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - assert_event_matches( - { - 'name': 'edx.badge.assertion.evidence_visited', - 'data': { - 'course_id': 'testorg/run1/refundable_course', - # pylint: disable=no-member - 'assertion_id': assertion.id, - 'assertion_json_url': 'http://www.example.com/assertion.json', - 'assertion_image_url': 'http://www.example.com/image.png', - 'user_id': self.user.id, - 'issuer': 'http://www.example.com/issuer.json', - 'enrollment_mode': 'honor', - }, - }, - self.get_event() - ) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) - def test_request_certificate_without_passing(self): - self.cert.status = CertificateStatuses.unavailable - self.cert.save() - request_certificate_url = reverse('certificates.views.request_certificate') - response = self.client.post(request_certificate_url, {'course_id': unicode(self.course.id)}) - self.assertEqual(response.status_code, 200) - response_json = json.loads(response.content) - self.assertEqual(CertificateStatuses.notpassing, response_json['add_status']) - - @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) - @override_settings(CERT_QUEUE='test-queue') - def test_request_certificate_after_passing(self): - self.cert.status = CertificateStatuses.unavailable - self.cert.save() - request_certificate_url = reverse('certificates.views.request_certificate') - with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue: - mock_queue.return_value = (0, "Successfully queued") - with patch('courseware.grades.grade') as mock_grade: - mock_grade.return_value = {'grade': 'Pass', 'percent': 0.75} - response = self.client.post(request_certificate_url, {'course_id': unicode(self.course.id)}) - self.assertEqual(response.status_code, 200) - response_json = json.loads(response.content) - self.assertEqual(CertificateStatuses.generating, response_json['add_status']) - - @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - @override_settings(LANGUAGE_CODE='fr') - def test_certificate_custom_template_with_org_mode_course(self): - """ - Tests custom template search and rendering. - """ - self._add_course_certificates(count=1, signatory_count=2) - self._create_custom_template(1, mode='honor', course_key=unicode(self.course.id)) - self._create_custom_template(2, mode='honor') - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - - with patch('certificates.api.get_course_organizations') as mock_get_orgs: - mock_get_orgs.side_effect = [ - [{"id": 1, "name": "organization name"}], - [{"id": 2, "name": "organization name 2"}], - ] - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'lang: fr') - self.assertContains(response, 'course name: {}'.format(self.course.display_name)) - # test with second organization template - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'lang: fr') - self.assertContains(response, 'course name: {}'.format(self.course.display_name)) - - @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_custom_template_with_org(self): - """ - Tests custom template search if if have a single template for all courses of organization. - """ - self._add_course_certificates(count=1, signatory_count=2) - self._create_custom_template(1) - self._create_custom_template(1, mode='honor') - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - - with patch('certificates.api.get_course_organizations') as mock_get_orgs: - mock_get_orgs.side_effect = [ - [{"id": 1, "name": "organization name"}], - ] - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'course name: {}'.format(self.course.display_name)) - - @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_custom_template_with_course_mode(self): - """ - Tests custom template search if if have a single template for a course mode. - """ - mode = 'honor' - self._add_course_certificates(count=1, signatory_count=2) - self._create_custom_template(mode=mode) - test_url = get_certificate_url( - user_id=self.user.id, - course_id=unicode(self.course.id) - ) - - with patch('certificates.api.get_course_organizations') as mock_get_orgs: - mock_get_orgs.return_value = [] - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'mode: {}'.format(mode)) - - class TrackShareRedirectTest(UrlResetMixin, ModuleStoreTestCase, EventTrackingTestCase): """ Verifies the badge image share event is sent out. diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py new file mode 100644 index 0000000000..417e5126bb --- /dev/null +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -0,0 +1,525 @@ +"""Tests for certificates views. """ + +import json +from uuid import uuid4 +from nose.plugins.attrib import attr +from mock import patch + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.test.client import Client +from django.test.utils import override_settings + +from openedx.core.lib.tests.assertions.events import assert_event_matches +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from track.tests import EventTrackingTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +from certificates.api import get_certificate_url +from certificates.models import ( + GeneratedCertificate, + BadgeAssertion, + CertificateStatuses, + CertificateSocialNetworks, + CertificateTemplate, +) + +from certificates.tests.factories import ( + CertificateHtmlViewConfigurationFactory, + LinkedInAddToProfileConfigurationFactory, +) +from util import organizations_helpers as organizations_api +from django.test.client import RequestFactory +import urllib + +FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() +FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True + +FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy() +FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False + +FEATURES_WITH_CUSTOM_CERTS_ENABLED = { + "CUSTOM_CERTIFICATE_TEMPLATES_ENABLED": True +} +FEATURES_WITH_CUSTOM_CERTS_ENABLED.update(FEATURES_WITH_CERTS_ENABLED) + + +@attr('shard_1') +class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): + """ + Tests for the certificates web/html views + """ + def setUp(self): + super(CertificatesViewsTests, self).setUp() + self.client = Client() + self.course = CourseFactory.create( + org='testorg', number='run1', display_name='refundable course' + ) + self.course_id = self.course.location.course_key + self.user = UserFactory.create( + email='joe_user@edx.org', + username='joeuser', + password='foo' + ) + self.user.profile.name = "Joe User" + self.user.profile.save() + self.client.login(username=self.user.username, password='foo') + self.request = RequestFactory().request() + + self.cert = GeneratedCertificate.objects.create( + user=self.user, + course_id=self.course_id, + verify_uuid=uuid4(), + download_uuid=uuid4(), + download_url="http://www.example.com/certificates/download", + grade="0.95", + key='the_key', + distinction=True, + status='generated', + mode='honor', + name=self.user.profile.name, + ) + CourseEnrollmentFactory.create( + user=self.user, + course_id=self.course_id + ) + CertificateHtmlViewConfigurationFactory.create() + LinkedInAddToProfileConfigurationFactory.create() + + def _add_course_certificates(self, count=1, signatory_count=0, is_active=True): + """ + Create certificate for the course. + """ + signatories = [ + { + 'name': 'Signatory_Name ' + str(i), + 'title': 'Signatory_Title ' + str(i), + 'organization': 'Signatory_Organization ' + str(i), + 'signature_image_path': '/static/certificates/images/demo-sig{}.png'.format(i), + 'id': i, + } for i in xrange(signatory_count) + + ] + + certificates = [ + { + 'id': i, + 'name': 'Name ' + str(i), + 'description': 'Description ' + str(i), + 'course_title': 'course_title_' + str(i), + 'org_logo_path': '/t4x/orgX/testX/asset/org-logo-{}.png'.format(i), + 'signatories': signatories, + 'version': 1, + 'is_active': is_active + } for i in xrange(count) + ] + + self.course.certificates = {'certificates': certificates} + self.course.cert_html_view_enabled = True + self.course.save() + self.store.update_item(self.course, self.user.id) + + def _create_custom_template(self, org_id=None, mode=None, course_key=None): + """ + Creates a custom certificate template entry in DB. + """ + template_html = """ + <html> + <body> + lang: ${LANGUAGE_CODE} + course name: ${accomplishment_copy_course_name} + mode: ${course_mode} + ${accomplishment_copy_course_description} + </body> + </html> + """ + template = CertificateTemplate( + name='custom template', + template=template_html, + organization_id=org_id, + course_key=course_key, + mode=mode, + is_active=True + ) + template.save() + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_linkedin_share_url(self): + """ + Test: LinkedIn share URL. + """ + self._add_course_certificates(count=1, signatory_count=1, is_active=True) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertTrue(urllib.quote_plus(self.request.build_absolute_uri(test_url)) in response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_rendering_course_organization_data(self): + """ + Test: organization data should render on certificate web view if course has organization. + """ + test_organization_data = { + 'name': 'test organization', + 'short_name': 'test_organization', + 'description': 'Test Organization Description', + 'active': True, + 'logo': '/logo_test1.png/' + } + test_org = organizations_api.add_organization(organization_data=test_organization_data) + organizations_api.add_organization_course(organization_data=test_org, course_id=unicode(self.course.id)) + self._add_course_certificates(count=1, signatory_count=1, is_active=True) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertIn( + 'a course of study offered by test_organization, an online learning initiative of test organization', + response.content + ) + self.assertNotIn( + 'a course of study offered by testorg', + response.content + ) + self.assertIn( + '<title>test_organization {} Certificate |'.format(self.course.number, ), + response.content + ) + self.assertIn('logo_test1.png', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_valid_certificate(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + self._add_course_certificates(count=1, signatory_count=2) + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + # Hit any "verified" mode-specific branches + self.cert.mode = 'verified' + self.cert.save() + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + # Hit any 'xseries' mode-specific branches + self.cert.mode = 'xseries' + self.cert.save() + response = self.client.get(test_url) + self.assertIn(str(self.cert.verify_uuid), response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_with_valid_signatories(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + self._add_course_certificates(count=1, signatory_count=2) + response = self.client.get(test_url) + self.assertIn('course_title_0', response.content) + self.assertIn('Signatory_Name 0', response.content) + self.assertIn('Signatory_Title 0', response.content) + self.assertIn('Signatory_Organization 0', response.content) + self.assertIn('/static/certificates/images/demo-sig0.png', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_course_display_name_not_override_with_course_title(self): + # if certificate in descriptor has not course_title then course name should not be overridden with this title. + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + test_certificates = [ + { + 'id': 0, + 'name': 'Name 0', + 'description': 'Description 0', + 'signatories': [], + 'version': 1, + 'is_active':True + } + ] + self.course.certificates = {'certificates': test_certificates} + self.course.cert_html_view_enabled = True + self.course.save() + self.store.update_item(self.course, self.user.id) + response = self.client.get(test_url) + self.assertNotIn('test_course_title_0', response.content) + self.assertIn('refundable course', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_certificate_view_without_org_logo(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + test_certificates = [ + { + 'id': 0, + 'name': 'Certificate Name 0', + 'signatories': [], + 'version': 1, + 'is_active': True + } + ] + self.course.certificates = {'certificates': test_certificates} + self.course.cert_html_view_enabled = True + self.course.save() + self.store.update_item(self.course, self.user.id) + response = self.client.get(test_url) + # make sure response html has only one organization logo container for edX + self.assertContains(response, "<li class=\"wrapper-organization\">", 1) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_without_signatories(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course) + ) + self._add_course_certificates(count=1, signatory_count=0) + response = self.client.get(test_url) + self.assertNotIn('Signatory_Name 0', response.content) + self.assertNotIn('Signatory_Title 0', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) + def test_render_html_view_disabled_feature_flag_returns_static_url(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + self.assertIn(str(self.cert.download_url), test_url) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_course_id(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id='az/23423/4vs' + ) + + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_course(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id='missing/course/key' + ) + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_user(self): + test_url = get_certificate_url( + user_id=111, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_user_certificate(self): + self.cert.delete() + self.assertEqual(len(GeneratedCertificate.objects.all()), 0) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertIn('invalid', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_with_preview_mode(self): + """ + test certificate web view should render properly along with its signatories information when accessing it in + preview mode. Either the certificate is marked active or not. + """ + self.cert.delete() + self.assertEqual(len(GeneratedCertificate.objects.all()), 0) + self._add_course_certificates(count=1, signatory_count=2) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url + '?preview=honor') + self.assertNotIn(self.course.display_name, response.content) + self.assertIn('course_title_0', response.content) + self.assertIn('Signatory_Title 0', response.content) + + # mark certificate inactive but accessing in preview mode. + self._add_course_certificates(count=1, signatory_count=2, is_active=False) + response = self.client.get(test_url + '?preview=honor') + self.assertNotIn(self.course.display_name, response.content) + self.assertIn('course_title_0', response.content) + self.assertIn('Signatory_Title 0', response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_render_html_view_invalid_certificate_configuration(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertIn("Invalid Certificate", response.content) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_certificate_evidence_event_emitted(self): + self.client.logout() + self._add_course_certificates(count=1, signatory_count=2) + self.recreate_tracker() + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + actual_event = self.get_event() + self.assertEqual(actual_event['name'], 'edx.certificate.evidence_visited') + assert_event_matches( + { + 'user_id': self.user.id, + 'certificate_id': unicode(self.cert.verify_uuid), + 'enrollment_mode': self.cert.mode, + 'certificate_url': test_url, + 'course_id': unicode(self.course.id), + 'social_network': CertificateSocialNetworks.linkedin + }, + actual_event['data'] + ) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_evidence_event_sent(self): + cert_url = get_certificate_url( + user_id=self.user.id, + course_id=self.course_id + ) + test_url = '{}?evidence_visit=1'.format(cert_url) + self._add_course_certificates(count=1, signatory_count=2) + self.recreate_tracker() + assertion = BadgeAssertion( + user=self.user, course_id=self.course_id, mode='honor', + data={ + 'image': 'http://www.example.com/image.png', + 'json': {'id': 'http://www.example.com/assertion.json'}, + 'issuer': 'http://www.example.com/issuer.json', + + } + ) + assertion.save() + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + assert_event_matches( + { + 'name': 'edx.badge.assertion.evidence_visited', + 'data': { + 'course_id': 'testorg/run1/refundable_course', + # pylint: disable=no-member + 'assertion_id': assertion.id, + 'assertion_json_url': 'http://www.example.com/assertion.json', + 'assertion_image_url': 'http://www.example.com/image.png', + 'user_id': self.user.id, + 'issuer': 'http://www.example.com/issuer.json', + 'enrollment_mode': 'honor', + }, + }, + self.get_event() + ) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) + def test_request_certificate_without_passing(self): + self.cert.status = CertificateStatuses.unavailable + self.cert.save() + request_certificate_url = reverse('certificates.views.request_certificate') + response = self.client.post(request_certificate_url, {'course_id': unicode(self.course.id)}) + self.assertEqual(response.status_code, 200) + response_json = json.loads(response.content) + self.assertEqual(CertificateStatuses.notpassing, response_json['add_status']) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) + @override_settings(CERT_QUEUE='test-queue') + def test_request_certificate_after_passing(self): + self.cert.status = CertificateStatuses.unavailable + self.cert.save() + request_certificate_url = reverse('certificates.views.request_certificate') + with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue: + mock_queue.return_value = (0, "Successfully queued") + with patch('courseware.grades.grade') as mock_grade: + mock_grade.return_value = {'grade': 'Pass', 'percent': 0.75} + response = self.client.post(request_certificate_url, {'course_id': unicode(self.course.id)}) + self.assertEqual(response.status_code, 200) + response_json = json.loads(response.content) + self.assertEqual(CertificateStatuses.generating, response_json['add_status']) + + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + @override_settings(LANGUAGE_CODE='fr') + def test_certificate_custom_template_with_org_mode_course(self): + """ + Tests custom template search and rendering. + """ + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(1, mode='honor', course_key=unicode(self.course.id)) + self._create_custom_template(2, mode='honor') + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.side_effect = [ + [{"id": 1, "name": "organization name"}], + [{"id": 2, "name": "organization name 2"}], + ] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'lang: fr') + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + # test with second organization template + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'lang: fr') + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + def test_certificate_custom_template_with_org(self): + """ + Tests custom template search if if have a single template for all courses of organization. + """ + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(1) + self._create_custom_template(1, mode='honor') + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.side_effect = [ + [{"id": 1, "name": "organization name"}], + ] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'course name: {}'.format(self.course.display_name)) + + @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) + def test_certificate_custom_template_with_course_mode(self): + """ + Tests custom template search if if have a single template for a course mode. + """ + mode = 'honor' + self._add_course_certificates(count=1, signatory_count=2) + self._create_custom_template(mode=mode) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + + with patch('certificates.api.get_course_organizations') as mock_get_orgs: + mock_get_orgs.return_value = [] + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'mode: {}'.format(mode)) diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 2396fe4ee6..f4e010df73 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -87,12 +87,16 @@ def _update_certificate_context(context, course, user, user_certificate): user_fullname = user.profile.name platform_name = microsite.get_value("platform_name", settings.PLATFORM_NAME) certificate_type = context.get('certificate_type') - partner_name = course.org + partner_short_name = course.org + partner_long_name = None organizations = organization_api.get_course_organizations(course_id=course.id) if organizations: #TODO Need to add support for multiple organizations, Currently we are interested in the first one. organization = organizations[0] - partner_name = organization.get('name', course.org) + partner_long_name = organization.get('name', partner_long_name) + partner_short_name = organization.get('short_name', partner_short_name) + context['organization_long_name'] = partner_long_name + context['organization_short_name'] = partner_short_name context['organization_logo'] = organization.get('logo', None) context['username'] = user.username @@ -100,7 +104,7 @@ def _update_certificate_context(context, course, user, user_certificate): context['accomplishment_user_id'] = user.id context['accomplishment_copy_name'] = user_fullname context['accomplishment_copy_username'] = user.username - context['accomplishment_copy_course_org'] = partner_name + context['accomplishment_copy_course_org'] = partner_short_name context['accomplishment_copy_course_name'] = course.display_name context['course_image_url'] = course_image_url(course) context['share_settings'] = settings.FEATURES.get('SOCIAL_SHARING_SETTINGS', {}) @@ -126,11 +130,20 @@ def _update_certificate_context(context, course, user, user_certificate): year=user_certificate.modified_date.year ) - context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_name}, ' - 'through {platform_name}.').format( - partner_name=partner_name, - platform_name=platform_name - ) + if partner_long_name: + context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, an ' + 'online learning initiative of {partner_long_name} ' + 'through {platform_name}.').format( + partner_short_name=partner_short_name, + partner_long_name=partner_long_name, + platform_name=platform_name + ) + else: + context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, ' + 'through {platform_name}.').format( + partner_short_name=partner_short_name, + platform_name=platform_name + ) # Translators: Accomplishments describe the awards/certifications obtained by students on this platform context['accomplishment_copy_about'] = _('About {platform_name} Accomplishments').format( @@ -201,16 +214,16 @@ def _update_certificate_context(context, course, user, user_certificate): # Translators: This text represents the verification of the certificate context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, ' - 'who participated in {partner_name} {course_number}').format( + 'who participated in {partner_short_name} {course_number}').format( platform_name=platform_name, user_name=user_fullname, - partner_name=partner_name, + partner_short_name=partner_short_name, course_number=course.number ) # Translators: This text is bound to the HTML 'title' element of the page and appears in the browser title bar - context['document_title'] = _("{partner_name} {course_number} Certificate | {platform_name}").format( - partner_name=partner_name, + context['document_title'] = _("{partner_short_name} {course_number} Certificate | {platform_name}").format( + partner_short_name=partner_short_name, course_number=course.number, platform_name=platform_name ) diff --git a/lms/djangoapps/course_structure_api/v0/tests.py b/lms/djangoapps/course_structure_api/v0/tests.py index 3394450f79..c74f76f601 100644 --- a/lms/djangoapps/course_structure_api/v0/tests.py +++ b/lms/djangoapps/course_structure_api/v0/tests.py @@ -16,7 +16,7 @@ from opaque_keys.edx.locator import CourseLocator from xmodule.error_module import ErrorDescriptor from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.xml import CourseLocationManager from xmodule.tests import get_test_system @@ -38,7 +38,6 @@ class CourseViewTestsMixin(object): def setUp(self): super(CourseViewTestsMixin, self).setUp() - self.create_test_data() self.create_user_and_access_token() def create_user(self): @@ -49,9 +48,10 @@ class CourseViewTestsMixin(object): self.oauth_client = ClientFactory.create() self.access_token = AccessTokenFactory.create(user=self.user, client=self.oauth_client).token - def create_test_data(self): - self.invalid_course_id = 'foo/bar/baz' - self.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=[ + @classmethod + def create_course_data(cls): + cls.invalid_course_id = 'foo/bar/baz' + cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=[ { "min_count": 24, "weight": 0.2, @@ -67,40 +67,40 @@ class CourseViewTestsMixin(object): "short_label": "Exam" } ]) - self.course_id = unicode(self.course.id) + cls.course_id = unicode(cls.course.id) + with cls.store.bulk_operations(cls.course.id, emit_signals=False): + cls.sequential = ItemFactory.create( + category="sequential", + parent_location=cls.course.location, + display_name="Lesson 1", + format="Homework", + graded=True + ) - self.sequential = ItemFactory.create( - category="sequential", - parent_location=self.course.location, - display_name="Lesson 1", - format="Homework", - graded=True - ) + factory = MultipleChoiceResponseXMLFactory() + args = {'choices': [False, True, False]} + problem_xml = factory.build_xml(**args) + cls.problem = ItemFactory.create( + category="problem", + parent_location=cls.sequential.location, + display_name="Problem 1", + format="Homework", + data=problem_xml, + ) - factory = MultipleChoiceResponseXMLFactory() - args = {'choices': [False, True, False]} - problem_xml = factory.build_xml(**args) - self.problem = ItemFactory.create( - category="problem", - parent_location=self.sequential.location, - display_name="Problem 1", - format="Homework", - data=problem_xml, - ) + cls.video = ItemFactory.create( + category="video", + parent_location=cls.sequential.location, + display_name="Video 1", + ) - self.video = ItemFactory.create( - category="video", - parent_location=self.sequential.location, - display_name="Video 1", - ) + cls.html = ItemFactory.create( + category="html", + parent_location=cls.sequential.location, + display_name="HTML 1", + ) - self.html = ItemFactory.create( - category="html", - parent_location=self.sequential.location, - display_name="HTML 1", - ) - - self.empty_course = CourseFactory.create( + cls.empty_course = CourseFactory.create( start=datetime(2014, 6, 16, 14, 30), end=datetime(2015, 1, 16), org="MTD", @@ -208,9 +208,14 @@ class CourseDetailTestMixin(object): self.assertEqual(response.status_code, 404) -class CourseListTests(CourseViewTestsMixin, ModuleStoreTestCase): +class CourseListTests(CourseViewTestsMixin, SharedModuleStoreTestCase): view = 'course_structure_api:v0:list' + @classmethod + def setUpClass(cls): + super(CourseListTests, cls).setUpClass() + cls.create_course_data() + def test_get(self): """ The view should return a list of all courses. @@ -219,7 +224,6 @@ class CourseListTests(CourseViewTestsMixin, ModuleStoreTestCase): self.assertEqual(response.status_code, 200) data = response.data courses = data['results'] - self.assertEqual(len(courses), 2) self.assertEqual(data['count'], 2) self.assertEqual(data['num_pages'], 1) @@ -299,17 +303,27 @@ class CourseListTests(CourseViewTestsMixin, ModuleStoreTestCase): self.test_get() -class CourseDetailTests(CourseDetailTestMixin, CourseViewTestsMixin, ModuleStoreTestCase): +class CourseDetailTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase): view = 'course_structure_api:v0:detail' + @classmethod + def setUpClass(cls): + super(CourseDetailTests, cls).setUpClass() + cls.create_course_data() + def test_get(self): response = super(CourseDetailTests, self).test_get() self.assertValidResponseCourse(response.data, self.course) -class CourseStructureTests(CourseDetailTestMixin, CourseViewTestsMixin, ModuleStoreTestCase): +class CourseStructureTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase): view = 'course_structure_api:v0:structure' + @classmethod + def setUpClass(cls): + super(CourseStructureTests, cls).setUpClass() + cls.create_course_data() + def setUp(self): super(CourseStructureTests, self).setUp() @@ -363,9 +377,14 @@ class CourseStructureTests(CourseDetailTestMixin, CourseViewTestsMixin, ModuleSt self.assertDictEqual(response.data, expected) -class CourseGradingPolicyTests(CourseDetailTestMixin, CourseViewTestsMixin, ModuleStoreTestCase): +class CourseGradingPolicyTests(CourseDetailTestMixin, CourseViewTestsMixin, SharedModuleStoreTestCase): view = 'course_structure_api:v0:grading_policy' + @classmethod + def setUpClass(cls): + super(CourseGradingPolicyTests, cls).setUpClass() + cls.create_course_data() + def test_get(self): """ The view should return grading policy for a course. @@ -480,6 +499,7 @@ class CourseBlocksOrNavigationTestMixin(CourseDetailTestMixin, CourseViewTestsMi response = self.http_get_for_course(data={'block_json': 'incorrect'}) self.assertEqual(response.status_code, 400) + @SharedModuleStoreTestCase.modifies_courseware def test_no_access_to_block(self): """ Verifies the view returns only the top-level course block, excluding the sequential block @@ -576,15 +596,20 @@ class CourseNavigationTestMixin(object): self.assertEquals(len(block['descendants']), expected_num_descendants) -class CourseBlocksTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, ModuleStoreTestCase): +class CourseBlocksTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, SharedModuleStoreTestCase): """ A Test class for testing the Course 'blocks' view. """ block_navigation_view_type = 'blocks' container_fields = ['children'] + @classmethod + def setUpClass(cls): + super(CourseBlocksTests, cls).setUpClass() + cls.create_course_data() -class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, ModuleStoreTestCase): + +class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, SharedModuleStoreTestCase): """ A Test class for testing the Course 'navigation' view. """ @@ -592,11 +617,21 @@ class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationT container_fields = ['descendants'] block_fields = [] + @classmethod + def setUpClass(cls): + super(CourseNavigationTests, cls).setUpClass() + cls.create_course_data() + class CourseBlocksAndNavigationTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin, - CourseNavigationTestMixin, ModuleStoreTestCase): + CourseNavigationTestMixin, SharedModuleStoreTestCase): """ A Test class for testing the Course 'blocks+navigation' view. """ block_navigation_view_type = 'blocks+navigation' container_fields = ['children', 'descendants'] + + @classmethod + def setUpClass(cls): + super(CourseBlocksAndNavigationTests, cls).setUpClass() + cls.create_course_data() diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 58c0e90040..d41edcc613 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -115,7 +115,8 @@ Feature: LMS.Answer problems | drop down | incorrect | never | | multiple choice | incorrect | never | | checkbox | incorrect | never | - | radio | incorrect | never | + # TE-572 + #| radio | incorrect | never | #| string | incorrect | never | | numerical | incorrect | never | | formula | incorrect | never | diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 86120f62c4..3f9141d9aa 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -127,6 +127,51 @@ class MaxScoresCache(object): return max_score +class ProgressSummary(object): + """ + Wrapper class for the computation of a user's scores across a course. + + Attributes + chapters: a summary of all sections with problems in the course. It is + organized as an array of chapters, each containing an array of sections, + each containing an array of scores. This contains information for graded + and ungraded problems, and is good for displaying a course summary with + due dates, etc. + + weighted_scores: a dictionary mapping module locations to weighted Score + objects. + + locations_to_children: a dictionary mapping module locations to their + direct descendants. + """ + def __init__(self, chapters, weighted_scores, locations_to_children): + self.chapters = chapters + self.weighted_scores = weighted_scores + self.locations_to_children = locations_to_children + + def score_for_module(self, location): + """ + Calculate the aggregate weighted score for any location in the course. + This method returns a tuple containing (earned_score, possible_score). + + If the location is of 'problem' type, this method will return the + possible and earned scores for that problem. If the location refers to a + composite module (a vertical or section ) the scores will be the sums of + all scored problems that are children of the chosen location. + """ + if location in self.weighted_scores: + score = self.weighted_scores[location] + return score.earned, score.possible + children = self.locations_to_children[location] + earned = 0.0 + possible = 0.0 + for child in children: + child_earned, child_possible = self.score_for_module(child) + earned += child_earned + possible += child_possible + return earned, possible + + def descriptor_affects_grading(block_types_affecting_grading, descriptor): """ Returns True if the descriptor could have any impact on grading, else False. @@ -459,6 +504,21 @@ def progress_summary(student, request, course, field_data_cache=None, scores_cli in case there are unanticipated errors. """ with manual_transaction(): + progress = _progress_summary(student, request, course, field_data_cache, scores_client) + if progress: + return progress.chapters + else: + return None + + +@transaction.commit_manually +def get_weighted_scores(student, course, field_data_cache=None, scores_client=None): + """ + Uses the _progress_summary method to return a ProgressSummmary object + containing details of a students weighted scores for the course. + """ + with manual_transaction(): + request = _get_mock_request(student) return _progress_summary(student, request, course, field_data_cache, scores_client) @@ -509,6 +569,8 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations) chapters = [] + locations_to_children = defaultdict(list) + locations_to_weighted_scores = {} # Don't include chapters that aren't displayable (e.g. due to error) for chapter_module in course_module.get_display_items(): # Skip if the chapter is hidden @@ -516,7 +578,6 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl continue sections = [] - for section_module in chapter_module.get_display_items(): # Skip if the section is hidden with manual_transaction(): @@ -531,7 +592,7 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl for module_descriptor in yield_dynamic_descriptor_descendants( section_module, student.id, module_creator ): - course_id = course.id + locations_to_children[module_descriptor.parent].append(module_descriptor.location) (correct, total) = get_score( student, module_descriptor, @@ -543,16 +604,17 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl if correct is None and total is None: continue - scores.append( - Score( - correct, - total, - graded, - module_descriptor.display_name_with_default, - module_descriptor.location - ) + weighted_location_score = Score( + correct, + total, + graded, + module_descriptor.display_name_with_default, + module_descriptor.location ) + scores.append(weighted_location_score) + locations_to_weighted_scores[module_descriptor.location] = weighted_location_score + scores.reverse() section_total, _ = graders.aggregate_scores( scores, section_module.display_name_with_default) @@ -577,7 +639,7 @@ def _progress_summary(student, request, course, field_data_cache=None, scores_cl max_scores_cache.push_to_remote() - return chapters + return ProgressSummary(chapters, locations_to_weighted_scores, locations_to_children) def weighted_score(raw_correct, raw_total, weight): @@ -705,15 +767,10 @@ def iterate_grades_for(course_or_id, students, keep_raw_scores=False): else: course = course_or_id - # We make a fake request because grading code expects to be able to look at - # the request. We have to attach the correct user to the request before - # grading that student. - request = RequestFactory().get('/') - for student in students: with dog_stats_api.timer('lms.grades.iterate_grades_for', tags=[u'action:{}'.format(course.id)]): try: - request.user = student + request = _get_mock_request(student) # Grading calls problem rendering, which calls masquerading, # which checks session vars -- thus the empty session dict below. # It's not pretty, but untangling that is currently beyond the @@ -732,3 +789,14 @@ def iterate_grades_for(course_or_id, students, keep_raw_scores=False): exc.message ) yield student, {}, exc.message + + +def _get_mock_request(student): + """ + Make a fake request because grading code expects to be able to look at + the request. We have to attach the correct user to the request before + grading that student. + """ + request = RequestFactory().get('/') + request.user = student + return request diff --git a/lms/djangoapps/courseware/tests/test_course_survey.py b/lms/djangoapps/courseware/tests/test_course_survey.py index 7fa1960f33..6094d06c7f 100644 --- a/lms/djangoapps/courseware/tests/test_course_survey.py +++ b/lms/djangoapps/courseware/tests/test_course_survey.py @@ -4,10 +4,12 @@ Python tests for the Survey workflows from collections import OrderedDict from nose.plugins.attrib import attr +from copy import deepcopy from django.core.urlresolvers import reverse +from django.contrib.auth.models import User -from survey.models import SurveyForm +from survey.models import SurveyForm, SurveyAnswer from common.test.utils import XssTestMixin from xmodule.modulestore.tests.factories import CourseFactory @@ -65,6 +67,8 @@ class SurveyViewsTests(LoginEnrollmentTestCase, ModuleStoreTestCase, XssTestMixi self.enroll(self.course_without_survey, True) self.enroll(self.course_with_bogus_survey, True) + self.user = User.objects.get(email=email) + self.view_url = reverse('view_survey', args=[self.test_survey_name]) self.postback_url = reverse('submit_answers', args=[self.test_survey_name]) @@ -137,6 +141,52 @@ class SurveyViewsTests(LoginEnrollmentTestCase, ModuleStoreTestCase, XssTestMixi self._assert_no_redirect(self.course) + def test_course_id_field(self): + """ + Assert that the course_id will be in the form fields, if available + """ + + resp = self.client.get( + reverse( + 'course_survey', + kwargs={'course_id': unicode(self.course.id)} + ) + ) + + self.assertEqual(resp.status_code, 200) + expected = '<input type="hidden" name="course_id" value="{course_id}" />'.format( + course_id=unicode(self.course.id) + ) + + self.assertContains(resp, expected) + + def test_course_id_persists(self): + """ + Assert that a posted back course_id is stored in the database + """ + + answers = deepcopy(self.student_answers) + answers.update({ + 'course_id': unicode(self.course.id) + }) + + resp = self.client.post( + self.postback_url, + answers + ) + self.assertEquals(resp.status_code, 200) + + self._assert_no_redirect(self.course) + + # however we want to make sure we persist the course_id + answer_objs = SurveyAnswer.objects.filter( + user=self.user, + form=self.survey + ) + + for answer_obj in answer_objs: + self.assertEquals(answer_obj.course_key, self.course.id) + def test_visiting_course_with_bogus_survey(self): """ Verifies that going to the courseware with a required, but non-existing survey, does not redirect diff --git a/lms/djangoapps/courseware/tests/test_grades.py b/lms/djangoapps/courseware/tests/test_grades.py index 5935eaae03..9715cedf28 100644 --- a/lms/djangoapps/courseware/tests/test_grades.py +++ b/lms/djangoapps/courseware/tests/test_grades.py @@ -2,13 +2,15 @@ Test grade calculation. """ from django.http import Http404 +from django.test import TestCase from django.test.client import RequestFactory -from mock import patch +from mock import patch, MagicMock from nose.plugins.attrib import attr from opaque_keys.edx.locations import SlashSeparatedCourseKey +from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator -from courseware.grades import field_data_cache_for_grading, grade, iterate_grades_for, MaxScoresCache +from courseware.grades import field_data_cache_for_grading, grade, iterate_grades_for, MaxScoresCache, ProgressSummary from student.tests.factories import UserFactory from student.models import CourseEnrollment from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -194,3 +196,125 @@ class TestFieldDataCacheScorableLocations(ModuleStoreTestCase): self.assertNotIn('html', block_types) self.assertNotIn('discussion', block_types) self.assertIn('problem', block_types) + + +class TestProgressSummary(TestCase): + """ + Test the method that calculates the score for a given block based on the + cumulative scores of its children. This test class uses a hard-coded block + hierarchy with scores as follows: + a + +--------+--------+ + b c + +--------------+-----------+ | + d e f g + +-----+ +-----+-----+ | | + h i j k l m n + (2/5) (3/5) (0/1) - (1/3) - (3/10) + + """ + def setUp(self): + super(TestProgressSummary, self).setUp() + self.course_key = CourseLocator( + org='some_org', + course='some_course', + run='some_run' + ) + self.loc_a = self.create_location('chapter', 'a') + self.loc_b = self.create_location('section', 'b') + self.loc_c = self.create_location('section', 'c') + self.loc_d = self.create_location('vertical', 'd') + self.loc_e = self.create_location('vertical', 'e') + self.loc_f = self.create_location('vertical', 'f') + self.loc_g = self.create_location('vertical', 'g') + self.loc_h = self.create_location('problem', 'h') + self.loc_i = self.create_location('problem', 'i') + self.loc_j = self.create_location('problem', 'j') + self.loc_k = self.create_location('html', 'k') + self.loc_l = self.create_location('problem', 'l') + self.loc_m = self.create_location('html', 'm') + self.loc_n = self.create_location('problem', 'n') + + weighted_scores = { + self.loc_h: self.create_score(2, 5), + self.loc_i: self.create_score(3, 5), + self.loc_j: self.create_score(0, 1), + self.loc_l: self.create_score(1, 3), + self.loc_n: self.create_score(3, 10), + } + locations_to_scored_children = { + self.loc_a: [self.loc_h, self.loc_i, self.loc_j, self.loc_l, self.loc_n], + self.loc_b: [self.loc_h, self.loc_i, self.loc_j, self.loc_l], + self.loc_c: [self.loc_n], + self.loc_d: [self.loc_h, self.loc_i], + self.loc_e: [self.loc_j, self.loc_l], + self.loc_f: [], + self.loc_g: [self.loc_n], + self.loc_k: [], + self.loc_m: [], + } + self.progress_summary = ProgressSummary( + None, weighted_scores, locations_to_scored_children + ) + + def create_score(self, earned, possible): + """ + Create a new mock Score object with specified earned and possible values + """ + score = MagicMock() + score.possible = possible + score.earned = earned + return score + + def create_location(self, block_type, block_id): + """ + Create a new BlockUsageLocation with the given type and ID. + """ + return BlockUsageLocator( + course_key=self.course_key, block_type=block_type, block_id=block_id + ) + + def test_score_chapter(self): + earned, possible = self.progress_summary.score_for_module(self.loc_a) + self.assertEqual(earned, 9) + self.assertEqual(possible, 24) + + def test_score_section_many_leaves(self): + earned, possible = self.progress_summary.score_for_module(self.loc_b) + self.assertEqual(earned, 6) + self.assertEqual(possible, 14) + + def test_score_section_one_leaf(self): + earned, possible = self.progress_summary.score_for_module(self.loc_c) + self.assertEqual(earned, 3) + self.assertEqual(possible, 10) + + def test_score_vertical_two_leaves(self): + earned, possible = self.progress_summary.score_for_module(self.loc_d) + self.assertEqual(earned, 5) + self.assertEqual(possible, 10) + + def test_score_vertical_two_leaves_one_unscored(self): + earned, possible = self.progress_summary.score_for_module(self.loc_e) + self.assertEqual(earned, 1) + self.assertEqual(possible, 4) + + def test_score_vertical_no_score(self): + earned, possible = self.progress_summary.score_for_module(self.loc_f) + self.assertEqual(earned, 0) + self.assertEqual(possible, 0) + + def test_score_vertical_one_leaf(self): + earned, possible = self.progress_summary.score_for_module(self.loc_g) + self.assertEqual(earned, 3) + self.assertEqual(possible, 10) + + def test_score_leaf(self): + earned, possible = self.progress_summary.score_for_module(self.loc_h) + self.assertEqual(earned, 2) + self.assertEqual(possible, 5) + + def test_score_leaf_no_score(self): + earned, possible = self.progress_summary.score_for_module(self.loc_m) + self.assertEqual(earned, 0) + self.assertEqual(possible, 0) diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 2b45be59c1..031467a91f 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -140,6 +140,10 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl self.enroll(self.course) self.student_user = User.objects.get(email=self.student) self.factory = RequestFactory() + # Disable the score change signal to prevent other components from being pulled into tests. + signal_patch = patch('courseware.module_render.SCORE_CHANGED.send') + signal_patch.start() + self.addCleanup(signal_patch.stop) def add_dropdown_to_section(self, section_location, name, num_inputs=2): """ diff --git a/lms/djangoapps/courseware/user_state_client.py b/lms/djangoapps/courseware/user_state_client.py index ce0de871da..e6b70fa2f3 100644 --- a/lms/djangoapps/courseware/user_state_client.py +++ b/lms/djangoapps/courseware/user_state_client.py @@ -140,6 +140,8 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): block_count = state_length = 0 evt_time = time() + self._ddog_histogram(evt_time, 'get_many.blks_requested', len(block_keys)) + modules = self._get_student_modules(username, block_keys) for module, usage_key in modules: if module.state is None: diff --git a/lms/djangoapps/discussion_api/tests/test_serializers.py b/lms/djangoapps/discussion_api/tests/test_serializers.py index 636067b160..b46149a8ee 100644 --- a/lms/djangoapps/discussion_api/tests/test_serializers.py +++ b/lms/djangoapps/discussion_api/tests/test_serializers.py @@ -27,13 +27,19 @@ from lms.lib.comment_client.comment import Comment from lms.lib.comment_client.thread import Thread from student.tests.factories import UserFactory from util.testing import UrlResetMixin -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory @ddt.ddt class SerializerTestMixin(CommentsServiceMockMixin, UrlResetMixin): + @classmethod + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUpClass(cls): + super(SerializerTestMixin, cls).setUpClass() + cls.course = CourseFactory.create() + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(SerializerTestMixin, self).setUp() @@ -45,7 +51,6 @@ class SerializerTestMixin(CommentsServiceMockMixin, UrlResetMixin): self.register_get_user_response(self.user) self.request = RequestFactory().get("/dummy") self.request.user = self.user - self.course = CourseFactory.create() self.author = UserFactory.create() def create_role(self, role_name, users, course=None): @@ -128,7 +133,7 @@ class SerializerTestMixin(CommentsServiceMockMixin, UrlResetMixin): @ddt.ddt -class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase): +class ThreadSerializerSerializationTest(SerializerTestMixin, SharedModuleStoreTestCase): """Tests for ThreadSerializer serialization.""" def make_cs_content(self, overrides): """ @@ -245,7 +250,7 @@ class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase @ddt.ddt -class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase): +class CommentSerializerTest(SerializerTestMixin, SharedModuleStoreTestCase): """Tests for CommentSerializer.""" def setUp(self): super(CommentSerializerTest, self).setUp() @@ -402,15 +407,20 @@ class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase): @ddt.ddt -class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestCase): +class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleStoreTestCase): """Tests for ThreadSerializer deserialization.""" + @classmethod + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUpClass(cls): + super(ThreadSerializerDeserializationTest, cls).setUpClass() + cls.course = CourseFactory.create() + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self): super(ThreadSerializerDeserializationTest, self).setUp() httpretty.reset() httpretty.enable() self.addCleanup(httpretty.disable) - self.course = CourseFactory.create() self.user = UserFactory.create() self.register_get_user_response(self.user) self.request = RequestFactory().get("/dummy") @@ -592,14 +602,18 @@ class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixi @ddt.ddt -class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStoreTestCase): +class CommentSerializerDeserializationTest(CommentsServiceMockMixin, SharedModuleStoreTestCase): """Tests for ThreadSerializer deserialization.""" + @classmethod + def setUpClass(cls): + super(CommentSerializerDeserializationTest, cls).setUpClass() + cls.course = CourseFactory.create() + def setUp(self): super(CommentSerializerDeserializationTest, self).setUp() httpretty.reset() httpretty.enable() self.addCleanup(httpretty.disable) - self.course = CourseFactory.create() self.user = UserFactory.create() self.register_get_user_response(self.user) self.request = RequestFactory().get("/dummy") diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py index ad0b727acc..d6ace314d6 100644 --- a/lms/djangoapps/instructor/offline_gradecalc.py +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -13,19 +13,20 @@ from json import JSONEncoder from courseware import grades, models from courseware.courses import get_course_by_id from django.contrib.auth.models import User +from opaque_keys import OpaqueKey +from opaque_keys.edx.keys import UsageKey +from xmodule.graders import Score from instructor.utils import DummyRequest class MyEncoder(JSONEncoder): - - def _iterencode(self, obj, markers=None): - if isinstance(obj, tuple) and hasattr(obj, '_asdict'): - gen = self._iterencode_dict(obj._asdict(), markers) - else: - gen = JSONEncoder._iterencode(self, obj, markers) - for chunk in gen: - yield chunk + """ JSON Encoder that can encode OpaqueKeys """ + def default(self, obj): # pylint: disable=method-hidden + """ Encode an object that the default encoder hasn't been able to. """ + if isinstance(obj, OpaqueKey): + return unicode(obj) + return JSONEncoder.default(self, obj) def offline_grade_calculation(course_key): @@ -50,9 +51,15 @@ def offline_grade_calculation(course_key): request.session = {} gradeset = grades.grade(student, request, course, keep_raw_scores=True) - gs = enc.encode(gradeset) + # Convert Score namedtuples to dicts: + totaled_scores = gradeset['totaled_scores'] + for section in totaled_scores: + totaled_scores[section] = [score._asdict() for score in totaled_scores[section]] + gradeset['raw_scores'] = [score._asdict() for score in gradeset['raw_scores']] + # Encode as JSON and save: + gradeset_str = enc.encode(gradeset) ocg, _created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_key) - ocg.gradeset = gs + ocg.gradeset = gradeset_str ocg.save() print "%s done" % student # print statement used because this is run by a management command @@ -93,4 +100,17 @@ def student_grades(student, request, course, keep_raw_scores=False, use_offline= msg='Error: no offline gradeset available for {}, {}'.format(student, course.id) ) - return json.loads(ocg.gradeset) + gradeset = json.loads(ocg.gradeset) + # Convert score dicts back to Score tuples: + + def score_from_dict(encoded): + """ Given a formerly JSON-encoded Score tuple, return the Score tuple """ + if encoded['module_id']: + encoded['module_id'] = UsageKey.from_string(encoded['module_id']) + return Score(**encoded) + + totaled_scores = gradeset['totaled_scores'] + for section in totaled_scores: + totaled_scores[section] = [score_from_dict(score) for score in totaled_scores[section]] + gradeset['raw_scores'] = [score_from_dict(score) for score in gradeset['raw_scores']] + return gradeset diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 9dfc50f26e..230b0ffb4a 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4,6 +4,7 @@ Unit tests for instructor.api methods. """ import datetime import ddt +import functools import random import pytz import io @@ -28,6 +29,7 @@ from mock import Mock, patch from nose.tools import raises from nose.plugins.attrib import attr from opaque_keys.edx.locations import SlashSeparatedCourseKey +from opaque_keys.edx.locator import UsageKey from course_modes.models import CourseMode from courseware.models import StudentModule @@ -107,6 +109,12 @@ REPORTS_DATA = ( 'instructor_api_endpoint': 'get_proctored_exam_results', 'task_api_endpoint': 'instructor_task.api.submit_proctored_exam_results_report', 'extra_instructor_api_kwargs': {}, + }, + { + 'report_type': 'problem responses', + 'instructor_api_endpoint': 'get_problem_responses', + 'task_api_endpoint': 'instructor_task.api.submit_calculate_problem_responses_csv', + 'extra_instructor_api_kwargs': {}, } ) @@ -234,6 +242,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ('get_students_who_may_enroll', {}), ('get_exec_summary_report', {}), ('get_proctored_exam_results', {}), + ('get_problem_responses', {}), ] # Endpoints that only Instructors can access self.instructor_level_endpoints = [ @@ -286,6 +295,20 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest "Student should not be allowed to access endpoint " + endpoint ) + def _access_problem_responses_endpoint(self, msg): + """ + Access endpoint for problem responses report, ensuring that + UsageKey.from_string returns a problem key that the endpoint + can work with. + + msg: message to display if assertion fails. + """ + mock_problem_key = Mock(return_value=u'') + mock_problem_key.course_key = self.course.id + with patch.object(UsageKey, 'from_string') as patched_method: + patched_method.return_value = mock_problem_key + self._access_endpoint('get_problem_responses', {}, 200, msg) + def test_staff_level(self): """ Ensure that a staff member can't access instructor endpoints. @@ -301,6 +324,11 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest # TODO: make these work if endpoint in ['update_forum_role_membership', 'list_forum_members']: continue + elif endpoint == 'get_problem_responses': + self._access_problem_responses_endpoint( + "Staff member should be allowed to access endpoint " + endpoint + ) + continue self._access_endpoint( endpoint, args, @@ -330,6 +358,11 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest # TODO: make these work if endpoint in ['update_forum_role_membership']: continue + elif endpoint == 'get_problem_responses': + self._access_problem_responses_endpoint( + "Instructor should be allowed to access endpoint " + endpoint + ) + continue self._access_endpoint( endpoint, args, @@ -2288,6 +2321,78 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment self.assertEqual(res['total_used_codes'], used_codes) self.assertEqual(res['total_codes'], 5) + def test_get_problem_responses_invalid_location(self): + """ + Test whether get_problem_responses returns an appropriate status + message when users submit an invalid problem location. + """ + url = reverse( + 'get_problem_responses', + kwargs={'course_id': unicode(self.course.id)} + ) + problem_location = '' + + response = self.client.get(url, {'problem_location': problem_location}) + res_json = json.loads(response.content) + self.assertEqual(res_json, 'Could not find problem with this location.') + + def valid_problem_location(test): # pylint: disable=no-self-argument + """ + Decorator for tests that target get_problem_responses endpoint and + need to pretend user submitted a valid problem location. + """ + @functools.wraps(test) + def wrapper(self, *args, **kwargs): + """ + Run `test` method, ensuring that UsageKey.from_string returns a + problem key that the get_problem_responses endpoint can + work with. + """ + mock_problem_key = Mock(return_value=u'') + mock_problem_key.course_key = self.course.id + with patch.object(UsageKey, 'from_string') as patched_method: + patched_method.return_value = mock_problem_key + test(self, *args, **kwargs) + return wrapper + + @valid_problem_location + def test_get_problem_responses_successful(self): + """ + Test whether get_problem_responses returns an appropriate status + message if CSV generation was started successfully. + """ + url = reverse( + 'get_problem_responses', + kwargs={'course_id': unicode(self.course.id)} + ) + problem_location = '' + + response = self.client.get(url, {'problem_location': problem_location}) + res_json = json.loads(response.content) + self.assertIn('status', res_json) + status = res_json['status'] + self.assertIn('is being created', status) + self.assertNotIn('already in progress', status) + + @valid_problem_location + def test_get_problem_responses_already_running(self): + """ + Test whether get_problem_responses returns an appropriate status + message if CSV generation is already in progress. + """ + url = reverse( + 'get_problem_responses', + kwargs={'course_id': unicode(self.course.id)} + ) + + with patch('instructor_task.api.submit_calculate_problem_responses_csv') as submit_task_function: + error = AlreadyRunningError() + submit_task_function.side_effect = error + response = self.client.get(url, {}) + res_json = json.loads(response.content) + self.assertIn('status', res_json) + self.assertIn('already in progress', res_json['status']) + def test_get_students_features(self): """ Test that some minimum of information is formatted @@ -2593,16 +2698,21 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment @ddt.data(*REPORTS_DATA) @ddt.unpack + @valid_problem_location def test_calculate_report_csv_success(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs): kwargs = {'course_id': unicode(self.course.id)} kwargs.update(extra_instructor_api_kwargs) url = reverse(instructor_api_endpoint, kwargs=kwargs) - - CourseFinanceAdminRole(self.course.id).add_users(self.instructor) - with patch(task_api_endpoint): - response = self.client.get(url, {}) success_status = "The {report_type} report is being created.".format(report_type=report_type) - self.assertIn(success_status, response.content) + if report_type == 'problem responses': + with patch(task_api_endpoint): + response = self.client.get(url, {'problem_location': ''}) + self.assertIn(success_status, response.content) + else: + CourseFinanceAdminRole(self.course.id).add_users(self.instructor) + with patch(task_api_endpoint): + response = self.client.get(url, {}) + self.assertIn(success_status, response.content) @ddt.data(*EXECUTIVE_SUMMARY_DATA) @ddt.unpack diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index a3f085dbc3..231eb533bd 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -37,9 +37,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from submissions import api as sub_api from student.models import anonymous_id_for_user -from xmodule.modulestore.tests.django_utils import ( - ModuleStoreTestCase, SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE -) +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE @attr('shard_1') @@ -399,7 +397,10 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): module_state_key=msk ).count(), 0) - def test_delete_submission_scores(self): + # Disable the score change signal to prevent other components from being + # pulled into tests. + @mock.patch('courseware.module_render.SCORE_CHANGED.send') + def test_delete_submission_scores(self, _lti_mock): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') @@ -576,23 +577,27 @@ class TestSendBetaRoleEmail(TestCase): @attr('shard_1') -class TestGetEmailParamsCCX(ModuleStoreTestCase): +class TestGetEmailParamsCCX(SharedModuleStoreTestCase): """ Test what URLs the function get_email_params for CCX student enrollment. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE + @classmethod + def setUpClass(cls): + super(TestGetEmailParamsCCX, cls).setUpClass() + cls.course = CourseFactory.create() + @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def setUp(self): super(TestGetEmailParamsCCX, self).setUp() - - self.course = CourseFactory.create() self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.course.id) role.add_users(self.coach) self.ccx = CcxFactory(course_id=self.course.id, coach=self.coach) self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) + # Explicitly construct what we expect the course URLs to be site = settings.SITE_NAME self.course_url = u'https://{}/courses/{}/'.format( @@ -600,9 +605,7 @@ class TestGetEmailParamsCCX(ModuleStoreTestCase): self.course_key ) self.course_about_url = self.course_url + 'about' - self.registration_url = u'https://{}/register'.format( - site, - ) + self.registration_url = u'https://{}/register'.format(site) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_ccx_enrollment_email_params(self): @@ -683,10 +686,14 @@ class TestRenderMessageToString(SharedModuleStoreTestCase): cls.subject_template = 'emails/enroll_email_allowedsubject.txt' cls.message_template = 'emails/enroll_email_allowedmessage.txt' + @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def setUp(self): super(TestRenderMessageToString, self).setUp() - self.course_key = None - self.ccx = None + coach = AdminFactory.create() + role = CourseCcxCoachRole(self.course.id) + role.add_users(coach) + self.ccx = CcxFactory(course_id=self.course.id, coach=coach) + self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) def get_email_params(self): """ @@ -702,12 +709,6 @@ class TestRenderMessageToString(SharedModuleStoreTestCase): """ Returns a dictionary of parameters used to render an email for CCX. """ - coach = AdminFactory.create() - role = CourseCcxCoachRole(self.course.id) - role.add_users(coach) - self.ccx = CcxFactory(course_id=self.course.id, coach=coach) - self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) - email_params = get_email_params( self.course, True, @@ -730,12 +731,10 @@ class TestRenderMessageToString(SharedModuleStoreTestCase): language=language ) - def get_subject_and_message_ccx(self): + def get_subject_and_message_ccx(self, subject_template, message_template): """ Returns the subject and message rendered in the specified language for CCX. """ - subject_template = 'emails/enroll_email_enrolledsubject.txt' - message_template = 'emails/enroll_email_enrolledmessage.txt' return render_message_to_string( subject_template, message_template, @@ -758,11 +757,15 @@ class TestRenderMessageToString(SharedModuleStoreTestCase): self.assertIn("You have been", message) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) - def test_render_message_ccx(self): + def test_render_enrollment_message_ccx_members(self): """ - Test email template renders for CCX. + Test enrollment email template renders for CCX. + For EDX members. """ - subject, message = self.get_subject_and_message_ccx() + subject_template = 'emails/enroll_email_enrolledsubject.txt' + message_template = 'emails/enroll_email_enrolledmessage.txt' + + subject, message = self.get_subject_and_message_ccx(subject_template, message_template) self.assertIn(self.ccx.display_name, subject) self.assertIn(self.ccx.display_name, message) site = settings.SITE_NAME @@ -771,3 +774,45 @@ class TestRenderMessageToString(SharedModuleStoreTestCase): self.course_key ) self.assertIn(course_url, message) + + @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) + def test_render_unenrollment_message_ccx_members(self): + """ + Test unenrollment email template renders for CCX. + For EDX members. + """ + subject_template = 'emails/unenroll_email_subject.txt' + message_template = 'emails/unenroll_email_enrolledmessage.txt' + + subject, message = self.get_subject_and_message_ccx(subject_template, message_template) + self.assertIn(self.ccx.display_name, subject) + self.assertIn(self.ccx.display_name, message) + + @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) + def test_render_enrollment_message_ccx_non_members(self): + """ + Test enrollment email template renders for CCX. + For non EDX members. + """ + subject_template = 'emails/enroll_email_allowedsubject.txt' + message_template = 'emails/enroll_email_allowedmessage.txt' + + subject, message = self.get_subject_and_message_ccx(subject_template, message_template) + self.assertIn(self.ccx.display_name, subject) + self.assertIn(self.ccx.display_name, message) + site = settings.SITE_NAME + registration_url = u'https://{}/register'.format(site) + self.assertIn(registration_url, message) + + @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) + def test_render_unenrollment_message_ccx_non_members(self): + """ + Test unenrollment email template renders for CCX. + For non EDX members. + """ + subject_template = 'emails/unenroll_email_subject.txt' + message_template = 'emails/unenroll_email_allowedmessage.txt' + + subject, message = self.get_subject_and_message_ccx(subject_template, message_template) + self.assertIn(self.ccx.display_name, subject) + self.assertIn(self.ccx.display_name, message) diff --git a/lms/djangoapps/instructor/tests/test_offline_gradecalc.py b/lms/djangoapps/instructor/tests/test_offline_gradecalc.py new file mode 100644 index 0000000000..cda1e03ec0 --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_offline_gradecalc.py @@ -0,0 +1,107 @@ +""" +Tests for offline_gradecalc.py +""" +import json +from mock import patch + +from courseware.models import OfflineComputedGrade +from student.models import CourseEnrollment +from student.tests.factories import UserFactory +from xmodule.graders import Score +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from ..offline_gradecalc import offline_grade_calculation, student_grades + + +def mock_grade(_student, _request, course, **_kwargs): + """ Return some fake grade data to mock grades.grade() """ + return { + 'grade': u'Pass', + 'totaled_scores': { + u'Homework': [ + Score(earned=10.0, possible=10.0, graded=True, section=u'Subsection 1', module_id=None), + ] + }, + 'percent': 0.85, + 'raw_scores': [ + Score( + earned=5.0, possible=5.0, graded=True, section=u'Numerical Input', + module_id=course.id.make_usage_key('problem', 'problem1'), + ), + Score( + earned=5.0, possible=5.0, graded=True, section=u'Multiple Choice', + module_id=course.id.make_usage_key('problem', 'problem2'), + ), + ], + 'section_breakdown': [ + {'category': u'Homework', 'percent': 1.0, 'detail': u'Homework 1 - Test - 100% (10/10)', 'label': u'HW 01'}, + {'category': u'Final Exam', 'prominent': True, 'percent': 0, 'detail': u'Final = 0%', 'label': u'Final'} + ], + 'grade_breakdown': [ + {'category': u'Homework', 'percent': 0.85, 'detail': u'Homework = 85.00% of a possible 85.00%'}, + {'category': u'Final Exam', 'percent': 0.0, 'detail': u'Final Exam = 0.00% of a possible 15.00%'} + ] + } + + +class TestOfflineGradeCalc(ModuleStoreTestCase): + """ Test Offline Grade Calculation with some mocked grades """ + def setUp(self): + super(TestOfflineGradeCalc, self).setUp() + + with modulestore().default_store(ModuleStoreEnum.Type.split): # Test with split b/c old mongo keys are messy + self.course = CourseFactory.create() + self.user = UserFactory.create() + CourseEnrollment.enroll(self.user, self.course.id) + + patcher = patch('courseware.grades.grade', new=mock_grade) + patcher.start() + self.addCleanup(patcher.stop) + + def test_output(self): + offline_grades = OfflineComputedGrade.objects + self.assertEqual(offline_grades.filter(user=self.user, course_id=self.course.id).count(), 0) + offline_grade_calculation(self.course.id) + result = offline_grades.get(user=self.user, course_id=self.course.id) + decoded = json.loads(result.gradeset) + self.assertEqual(decoded['grade'], "Pass") + self.assertEqual(decoded['percent'], 0.85) + self.assertEqual(decoded['totaled_scores'], { + "Homework": [ + {"earned": 10.0, "possible": 10.0, "graded": True, "section": "Subsection 1", "module_id": None} + ] + }) + self.assertEqual(decoded['raw_scores'], [ + { + "earned": 5.0, + "possible": 5.0, + "graded": True, + "section": "Numerical Input", + "module_id": unicode(self.course.id.make_usage_key('problem', 'problem1')), + }, + { + "earned": 5.0, + "possible": 5.0, + "graded": True, + "section": "Multiple Choice", + "module_id": unicode(self.course.id.make_usage_key('problem', 'problem2')), + } + ]) + self.assertEqual(decoded['section_breakdown'], [ + {"category": "Homework", "percent": 1.0, "detail": "Homework 1 - Test - 100% (10/10)", "label": "HW 01"}, + {"category": "Final Exam", "label": "Final", "percent": 0, "detail": "Final = 0%", "prominent": True} + ]) + self.assertEqual(decoded['grade_breakdown'], [ + {"category": "Homework", "percent": 0.85, "detail": "Homework = 85.00% of a possible 85.00%"}, + {"category": "Final Exam", "percent": 0.0, "detail": "Final Exam = 0.00% of a possible 15.00%"} + ]) + + def test_student_grades(self): + """ Test that the data returned by student_grades() and grades.grade() match """ + offline_grade_calculation(self.course.id) + with patch('courseware.grades.grade', side_effect=AssertionError('Should not re-grade')): + result = student_grades(self.user, None, self.course, use_offline=True) + self.assertEqual(result, mock_grade(self.user, None, self.course)) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index f4a4887d81..c559aadccb 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -35,7 +35,7 @@ from util.file import ( store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator ) -from util.json_request import JsonResponse +from util.json_request import JsonResponse, JsonResponseBadRequest from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features from microsite_configuration import microsite @@ -107,7 +107,7 @@ from .tools import ( bulk_email_is_enabled_for_course, add_block_ids, ) -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys import InvalidKeyError from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted @@ -887,6 +887,51 @@ def list_course_role_members(request, course_id): return JsonResponse(response_payload) +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +def get_problem_responses(request, course_id): + """ + Initiate generation of a CSV file containing all student answers + to a given problem. + + Responds with JSON + {"status": "... status message ..."} + + if initiation is successful (or generation task is already running). + + Responds with BadRequest if problem location is faulty. + """ + course_key = CourseKey.from_string(course_id) + problem_location = request.GET.get('problem_location', '') + + try: + problem_key = UsageKey.from_string(problem_location) + # Are we dealing with an "old-style" problem location? + run = getattr(problem_key, 'run') + if not run: + problem_key = course_key.make_usage_key_from_deprecated_string(problem_location) + if problem_key.course_key != course_key: + raise InvalidKeyError(type(problem_key), problem_key) + except InvalidKeyError: + return JsonResponseBadRequest(_("Could not find problem with this location.")) + + try: + instructor_task.api.submit_calculate_problem_responses_csv(request, course_key, problem_location) + success_status = _( + "The problem responses report is being created." + " To view the status of the report, see Pending Tasks below." + ) + return JsonResponse({"status": success_status}) + except AlreadyRunningError: + already_running_status = _( + "A problem responses report generation task is already in progress. " + "Check the 'Pending Tasks' table for the status of the task. " + "When completed, the report will be available for download in the table below." + ) + return JsonResponse({"status": already_running_status}) + + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_level('staff') diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 2d2afeb55b..f24367a159 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -17,6 +17,8 @@ urlpatterns = patterns( 'instructor.views.api.modify_access', name="modify_access"), url(r'^bulk_beta_modify_access$', 'instructor.views.api.bulk_beta_modify_access', name="bulk_beta_modify_access"), + url(r'^get_problem_responses$', + 'instructor.views.api.get_problem_responses', name="get_problem_responses"), url(r'^get_grading_config$', 'instructor.views.api.get_grading_config', name="get_grading_config"), url(r'^get_students_features(?P<csv>/csv)?$', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 6650f1e8d8..c3b96ae274 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -495,6 +495,7 @@ def _section_data_download(course, access): 'section_display_name': _('Data Download'), 'access': access, 'show_generate_proctored_exam_report_button': settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False), + 'get_problem_responses_url': reverse('get_problem_responses', kwargs={'course_id': unicode(course_key)}), 'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': unicode(course_key)}), 'get_students_features_url': reverse('get_students_features', kwargs={'course_id': unicode(course_key)}), 'get_students_who_may_enroll_url': reverse( diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index b9118e4ec1..ac9e07261c 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -276,35 +276,6 @@ def instructor_dashboard(request, course_id): msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 - #---------------------------------------- - # DataDump - - elif 'Download CSV of all responses to problem' in action: - problem_to_dump = request.POST.get('problem_to_dump', '') - - if problem_to_dump[-4:] == ".xml": - problem_to_dump = problem_to_dump[:-4] - try: - module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump) - smdat = StudentModule.objects.filter( - course_id=course_key, - module_state_key=module_state_key - ) - smdat = smdat.order_by('student') - msg += _("Found {num} records to dump.").format(num=smdat) - except Exception as err: # pylint: disable=broad-except - msg += "<font color='red'>{text}</font><pre>{err}</pre>".format( - text=_("Couldn't find module with that urlname."), - err=escape(err) - ) - smdat = [] - - if smdat: - datatable = {'header': ['username', 'state']} - datatable['data'] = [[x.student.username, x.state] for x in smdat] - datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) - return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable) - #---------------------------------------- # enrollment diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index 59af446680..8624d27b26 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -11,12 +11,14 @@ from shoppingcart.models import ( from django.db.models import Q from django.conf import settings from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -import xmodule.graders as xmgraders from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from opaque_keys.edx.keys import UsageKey +import xmodule.graders as xmgraders from microsite_configuration import microsite from student.models import CourseEnrollmentAllowed from edx_proctoring.api import get_all_exam_attempts +from courseware.models import StudentModule STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email') @@ -317,6 +319,41 @@ def coupon_codes_features(features, coupons_list, course_id): return [extract_coupon(coupon, features) for coupon in coupons_list] +def list_problem_responses(course_key, problem_location): + """ + Return responses to a given problem as a dict. + + list_problem_responses(course_key, problem_location) + + would return [ + {'username': u'user1', 'state': u'...'}, + {'username': u'user2', 'state': u'...'}, + {'username': u'user3', 'state': u'...'}, + ] + + where `state` represents a student's response to the problem + identified by `problem_location`. + """ + problem_key = UsageKey.from_string(problem_location) + # Are we dealing with an "old-style" problem location? + run = getattr(problem_key, 'run') + if not run: + problem_key = course_key.make_usage_key_from_deprecated_string(problem_location) + if problem_key.course_key != course_key: + return [] + + smdat = StudentModule.objects.filter( + course_id=course_key, + module_state_key=problem_key + ) + smdat = smdat.order_by('student') + + return [ + {'username': response.student.username, 'state': response.state} + for response in smdat + ] + + def course_registration_features(features, registration_codes, csv_type): """ Return list of Course Registration Codes as dictionaries. diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index 3b9595e41a..307a3eaaaf 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -2,27 +2,29 @@ Tests for instructor.basic """ -import json import datetime -from django.db.models import Q +import json import pytz -from student.models import CourseEnrollment, CourseEnrollmentAllowed +from mock import MagicMock, Mock, patch from django.core.urlresolvers import reverse -from mock import patch +from django.db.models import Q + +from course_modes.models import CourseMode +from courseware.tests.factories import InstructorFactory +from instructor_analytics.basic import ( + StudentModule, sale_record_features, sale_order_record_features, enrolled_students_features, + course_registration_features, coupon_codes_features, get_proctored_exam_results, list_may_enroll, + list_problem_responses, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES +) +from opaque_keys.edx.locator import UsageKey +from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory +from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.roles import CourseSalesAdminRole from student.tests.factories import UserFactory, CourseModeFactory from shoppingcart.models import ( CourseRegistrationCode, RegistrationCodeRedemption, Order, Invoice, Coupon, CourseRegCodeItem, CouponRedemption, CourseRegistrationCodeInvoiceItem ) -from course_modes.models import CourseMode -from instructor_analytics.basic import ( - sale_record_features, sale_order_record_features, enrolled_students_features, - course_registration_features, coupon_codes_features, list_may_enroll, - AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES, - get_proctored_exam_results) -from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory -from courseware.tests.factories import InstructorFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from edx_proctoring.api import create_exam @@ -51,6 +53,48 @@ class TestAnalyticsBasic(ModuleStoreTestCase): email=student.email, course_id=self.course_key ) + def test_list_problem_responses(self): + def result_factory(result_id): + """ + Return a dummy StudentModule object that can be queried for + relevant info (student.username and state). + """ + result = Mock(spec=['student', 'state']) + result.student.username.return_value = u'user{}'.format(result_id) + result.state.return_value = u'state{}'.format(result_id) + return result + + # Ensure that UsageKey.from_string returns a problem key that list_problem_responses can work with + # (even when called with a dummy location): + mock_problem_key = Mock(return_value=u'') + mock_problem_key.course_key = self.course_key + with patch.object(UsageKey, 'from_string') as patched_from_string: + patched_from_string.return_value = mock_problem_key + + # Ensure that StudentModule.objects.filter returns a result set that list_problem_responses can work with + # (this keeps us from having to create fixtures for this test): + mock_results = MagicMock(return_value=[result_factory(n) for n in range(5)]) + with patch.object(StudentModule, 'objects') as patched_manager: + patched_manager.filter.return_value = mock_results + + mock_problem_location = '' + problem_responses = list_problem_responses(self.course_key, problem_location=mock_problem_location) + + # Check if list_problem_responses called UsageKey.from_string to look up problem key: + patched_from_string.assert_called_once_with(mock_problem_location) + # Check if list_problem_responses called StudentModule.objects.filter to obtain relevant records: + patched_manager.filter.assert_called_once_with( + course_id=self.course_key, module_state_key=mock_problem_key + ) + + # Check if list_problem_responses returned expected results: + self.assertEqual(len(problem_responses), len(mock_results)) + for mock_result in mock_results: + self.assertTrue( + {'username': mock_result.student.username, 'state': mock_result.state} in + problem_responses + ) + def test_enrolled_students_features_username(self): self.assertIn('username', AVAILABLE_FEATURES) userreports = enrolled_students_features(self.course_key, ['username']) diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index de4c14f8c2..a1bb562536 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -18,6 +18,7 @@ from instructor_task.tasks import ( reset_problem_attempts, delete_problem_state, send_bulk_course_email, + calculate_problem_responses_csv, calculate_grades_csv, calculate_problem_grade_report, calculate_students_features_csv, @@ -328,6 +329,21 @@ def submit_bulk_course_email(request, course_key, email_id): return submit_task(request, task_type, task_class, course_key, task_input, task_key) +def submit_calculate_problem_responses_csv(request, course_key, problem_location): # pylint: disable=invalid-name + """ + Submits a task to generate a CSV file containing all student + answers to a given problem. + + Raises AlreadyRunningError if said file is already being updated. + """ + task_type = 'problem_responses_csv' + task_class = calculate_problem_responses_csv + task_input = {'problem_location': problem_location} + task_key = "" + + return submit_task(request, task_type, task_class, course_key, task_input, task_key) + + def submit_calculate_grades_csv(request, course_key): """ AlreadyRunningError is raised if the course's grades are already being updated. diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py index de5ab34f86..7f61b8ba52 100644 --- a/lms/djangoapps/instructor_task/tasks.py +++ b/lms/djangoapps/instructor_task/tasks.py @@ -34,6 +34,7 @@ from instructor_task.tasks_helper import ( rescore_problem_module_state, reset_attempts_module_state, delete_problem_module_state, + upload_problem_responses_csv, upload_grades_csv, upload_problem_grade_report, upload_students_csv, @@ -145,6 +146,18 @@ def send_bulk_course_email(entry_id, _xmodule_instance_args): return run_main_task(entry_id, visit_fcn, action_name) +@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable +def calculate_problem_responses_csv(entry_id, xmodule_instance_args): + """ + Compute student answers to a given problem and upload the CSV to + an S3 bucket for download. + """ + # Translators: This is a past-tense verb that is inserted into task progress messages as {action}. + action_name = ugettext_noop('generated') + task_fn = partial(upload_problem_responses_csv, xmodule_instance_args) + return run_main_task(entry_id, task_fn, action_name) + + @task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable def calculate_grades_csv(entry_id, xmodule_instance_args): """ diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index 0d9f72d68d..59c4463614 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -4,6 +4,7 @@ running state of a course. """ import json +import re from collections import OrderedDict from datetime import datetime from django.conf import settings @@ -46,7 +47,12 @@ from courseware.grades import iterate_grades_for from courseware.models import StudentModule from courseware.model_data import DjangoKeyValueStore, FieldDataCache from courseware.module_render import get_module_for_descriptor_internal -from instructor_analytics.basic import enrolled_students_features, list_may_enroll, get_proctored_exam_results +from instructor_analytics.basic import ( + enrolled_students_features, + get_proctored_exam_results, + list_may_enroll, + list_problem_responses +) from instructor_analytics.csvs import format_dictlist from instructor_task.models import ReportStore, InstructorTask, PROGRESS from lms.djangoapps.lms_xblock.runtime import LmsPartitionService @@ -849,6 +855,40 @@ def _order_problems(blocks): return problems +def upload_problem_responses_csv(_xmodule_instance_args, _entry_id, course_id, task_input, action_name): + """ + For a given `course_id`, generate a CSV file containing + all student answers to a given problem, and store using a `ReportStore`. + """ + start_time = time() + start_date = datetime.now(UTC) + num_reports = 1 + task_progress = TaskProgress(action_name, num_reports, start_time) + current_step = {'step': 'Calculating students answers to problem'} + task_progress.update_task_state(extra_meta=current_step) + + # Compute result table and format it + problem_location = task_input.get('problem_location') + student_data = list_problem_responses(course_id, problem_location) + features = ['username', 'state'] + header, rows = format_dictlist(student_data, features) + + task_progress.attempted = task_progress.succeeded = len(rows) + task_progress.skipped = task_progress.total - task_progress.attempted + + rows.insert(0, header) + + current_step = {'step': 'Uploading CSV'} + task_progress.update_task_state(extra_meta=current_step) + + # Perform the upload + problem_location = re.sub(r'[:/]', '_', problem_location) + csv_name = 'student_state_from_{}'.format(problem_location) + upload_csv_to_report_store(rows, csv_name, course_id, start_date) + + return task_progress.update_task_state(extra_meta=current_step) + + def upload_problem_grade_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): """ Generate a CSV containing all students' problem grades within a given diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 3145272484..113aeeb440 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -14,6 +14,7 @@ from instructor_task.api import ( submit_reset_problem_attempts_for_all_students, submit_delete_problem_state_for_all_students, submit_bulk_course_email, + submit_calculate_problem_responses_csv, submit_calculate_students_features_csv, submit_cohort_students, submit_detailed_enrollment_features_csv, @@ -203,6 +204,14 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa ) self._test_resubmission(api_call) + def test_submit_calculate_problem_responses(self): + api_call = lambda: submit_calculate_problem_responses_csv( + self.create_task_request(self.instructor), + self.course.id, + problem_location='' + ) + self._test_resubmission(api_call) + def test_submit_calculate_students_features(self): api_call = lambda: submit_calculate_students_features_csv( self.create_task_request(self.instructor), diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 12210c4260..c6701ebc55 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -33,6 +33,7 @@ from xmodule.partitions.partitions import Group, UserPartition from instructor_task.models import ReportStore from instructor_task.tasks_helper import ( cohort_students_and_upload, + upload_problem_responses_csv, upload_grades_csv, upload_problem_grade_report, upload_students_csv, @@ -277,6 +278,32 @@ class TestInstructorGradeReport(TestReportMixin, InstructorTaskCourseTestCase): self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result) +class TestProblemResponsesReport(TestReportMixin, InstructorTaskCourseTestCase): + """ + Tests that generation of CSV files listing student answers to a + given problem works. + """ + def setUp(self): + super(TestProblemResponsesReport, self).setUp() + self.course = CourseFactory.create() + + def test_success(self): + task_input = {'problem_location': ''} + with patch('instructor_task.tasks_helper._get_current_task'): + with patch('instructor_task.tasks_helper.list_problem_responses') as patched_data_source: + patched_data_source.return_value = [ + {'username': 'user0', 'state': u'state0'}, + {'username': 'user1', 'state': u'state1'}, + {'username': 'user2', 'state': u'state2'}, + ] + result = upload_problem_responses_csv(None, None, self.course.id, task_input, 'calculated') + report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD') + links = report_store.links_for(self.course.id) + + self.assertEquals(len(links), 1) + self.assertDictContainsSubset({'attempted': 3, 'succeeded': 3, 'failed': 0}, result) + + @ddt.ddt @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCourseTestCase): diff --git a/lms/djangoapps/lti_provider/admin.py b/lms/djangoapps/lti_provider/admin.py index d330d708c9..43aa7dac29 100644 --- a/lms/djangoapps/lti_provider/admin.py +++ b/lms/djangoapps/lti_provider/admin.py @@ -6,4 +6,10 @@ from django.contrib import admin from .models import LtiConsumer -admin.site.register(LtiConsumer) + +class LtiConsumerAdmin(admin.ModelAdmin): + """Admin for LTI Consumer""" + search_fields = ('consumer_name', 'consumer_key', 'instance_guid') + list_display = ('id', 'consumer_name', 'consumer_key', 'instance_guid') + +admin.site.register(LtiConsumer, LtiConsumerAdmin) diff --git a/lms/djangoapps/lti_provider/migrations/0004_add_version_to_graded_assignment.py b/lms/djangoapps/lti_provider/migrations/0004_add_version_to_graded_assignment.py new file mode 100644 index 0000000000..ae232a7571 --- /dev/null +++ b/lms/djangoapps/lti_provider/migrations/0004_add_version_to_graded_assignment.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# pylint: disable=invalid-name, missing-docstring, unused-argument, unused-import, line-too-long +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'GradedAssignment.version_number' + db.add_column('lti_provider_gradedassignment', 'version_number', + self.gf('django.db.models.fields.IntegerField')(default=0), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'GradedAssignment.version_number' + db.delete_column('lti_provider_gradedassignment', 'version_number') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'lti_provider.gradedassignment': { + 'Meta': {'unique_together': "(('outcome_service', 'lis_result_sourcedid'),)", 'object_name': 'GradedAssignment'}, + 'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lis_result_sourcedid': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'outcome_service': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lti_provider.OutcomeService']"}), + 'usage_key': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'version_number': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'lti_provider.lticonsumer': { + 'Meta': {'object_name': 'LtiConsumer'}, + 'consumer_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'consumer_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'consumer_secret': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_guid': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True', 'null': 'True'}) + }, + 'lti_provider.ltiuser': { + 'Meta': {'unique_together': "(('lti_consumer', 'lti_user_id'),)", 'object_name': 'LtiUser'}, + 'edx_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lti_consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lti_provider.LtiConsumer']"}), + 'lti_user_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'lti_provider.outcomeservice': { + 'Meta': {'object_name': 'OutcomeService'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lis_outcome_service_url': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'lti_consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lti_provider.LtiConsumer']"}) + } + } + + complete_apps = ['lti_provider'] diff --git a/lms/djangoapps/lti_provider/models.py b/lms/djangoapps/lti_provider/models.py index dc5305bfe5..23905e13c1 100644 --- a/lms/djangoapps/lti_provider/models.py +++ b/lms/djangoapps/lti_provider/models.py @@ -112,6 +112,7 @@ class GradedAssignment(models.Model): usage_key = UsageKeyField(max_length=255, db_index=True) outcome_service = models.ForeignKey(OutcomeService) lis_result_sourcedid = models.CharField(max_length=255, db_index=True) + version_number = models.IntegerField(default=0) class Meta(object): """ diff --git a/lms/djangoapps/lti_provider/outcomes.py b/lms/djangoapps/lti_provider/outcomes.py index bc02bcb3ec..5b228930c0 100644 --- a/lms/djangoapps/lti_provider/outcomes.py +++ b/lms/djangoapps/lti_provider/outcomes.py @@ -13,6 +13,7 @@ from lxml.builder import ElementMaker from oauthlib.oauth1 import Client from oauthlib.common import to_unicode import requests +from requests.exceptions import RequestException import requests_oauthlib from lti_provider.models import GradedAssignment, OutcomeService @@ -116,6 +117,61 @@ def generate_replace_result_xml(result_sourcedid, score): return etree.tostring(xml, xml_declaration=True, encoding='UTF-8') +def get_assignments_for_problem(problem_descriptor, user_id, course_key): + """ + Trace the parent hierarchy from a given problem to find all blocks that + correspond to graded assignment launches for this user. A problem may + show up multiple times for a given user; the problem could be embedded in + multiple courses (or multiple times in the same course), or the block could + be embedded more than once at different granularities (as an individual + problem and as a problem in a vertical, for example). + + Returns a list of GradedAssignment objects that are associated with the + given descriptor for the current user. + """ + locations = [] + current_descriptor = problem_descriptor + while current_descriptor: + locations.append(current_descriptor.location) + current_descriptor = current_descriptor.get_parent() + assignments = GradedAssignment.objects.filter( + user=user_id, course_key=course_key, usage_key__in=locations + ) + return assignments + + +def send_score_update(assignment, score): + """ + Create and send the XML message to the campus LMS system to update the grade + for a single graded assignment. + """ + xml = generate_replace_result_xml( + assignment.lis_result_sourcedid, score + ) + try: + response = sign_and_send_replace_result(assignment, xml) + except RequestException: + # failed to send result. 'response' is None, so more detail will be + # logged at the end of the method. + response = None + log.exception("Outcome Service: Error when sending result.") + + # If something went wrong, make sure that we have a complete log record. + # That way we can manually fix things up on the campus system later if + # necessary. + if not (response and check_replace_result_response(response)): + log.error( + "Outcome Service: Failed to update score on LTI consumer. " + "User: %s, course: %s, usage: %s, score: %s, status: %s, body: %s", + assignment.user, + assignment.course_key, + assignment.usage_key, + score, + response, + response.text if response else 'Unknown' + ) + + def sign_and_send_replace_result(assignment, xml): """ Take the XML document generated in generate_replace_result_xml, and sign it diff --git a/lms/djangoapps/lti_provider/tasks.py b/lms/djangoapps/lti_provider/tasks.py index 2db49f8c0d..4b774950c9 100644 --- a/lms/djangoapps/lti_provider/tasks.py +++ b/lms/djangoapps/lti_provider/tasks.py @@ -2,15 +2,19 @@ Asynchronous tasks for the LTI provider app. """ +from django.conf import settings +from django.contrib.auth.models import User from django.dispatch import receiver import logging -from requests.exceptions import RequestException +from courseware.grades import get_weighted_scores from courseware.models import SCORE_CHANGED from lms import CELERY_APP from lti_provider.models import GradedAssignment -import lti_provider.outcomes +import lti_provider.outcomes as outcomes from lti_provider.views import parse_course_and_usage_keys +from opaque_keys.edx.keys import CourseKey +from xmodule.modulestore.django import modulestore log = logging.getLogger("edx.lti_provider") @@ -28,13 +32,18 @@ def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument usage_id = kwargs.get('usage_id', None) if None not in (points_earned, points_possible, user_id, course_id, user_id): - send_outcome.delay( - points_possible, - points_earned, - user_id, - course_id, - usage_id - ) + course_key, usage_key = parse_course_and_usage_keys(course_id, usage_id) + assignments = increment_assignment_versions(course_key, usage_key, user_id) + for assignment in assignments: + if assignment.usage_key == usage_key: + send_leaf_outcome.delay( + assignment.id, points_earned, points_possible + ) + else: + send_composite_outcome.apply_async( + (user_id, course_id, assignment.id, assignment.version_number), + countdown=settings.LTI_AGGREGATE_SCORE_PASSBACK_DELAY + ) else: log.error( "Outcome Service: Required signal parameter is None. " @@ -44,55 +53,86 @@ def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument ) -@CELERY_APP.task(name='lti_provider.tasks.send_outcome') -def send_outcome(points_possible, points_earned, user_id, course_id, usage_id): +def increment_assignment_versions(course_key, usage_key, user_id): """ - Calculate the score for a given user in a problem and send it to the - appropriate LTI consumer's outcome service. + Update the version numbers for all assignments that are affected by a score + change event. Returns a list of all affected assignments. """ - course_key, usage_key = parse_course_and_usage_keys(course_id, usage_id) - assignments = GradedAssignment.objects.filter( - user=user_id, course_key=course_key, usage_key=usage_key + problem_descriptor = modulestore().get_item(usage_key) + # Get all assignments involving the current problem for which the campus LMS + # is expecting a grade. There may be many possible graded assignments, if + # a problem has been added several times to a course at different + # granularities (such as the unit or the vertical). + assignments = outcomes.get_assignments_for_problem( + problem_descriptor, user_id, course_key ) - - # Calculate the user's score, on a scale of 0.0 - 1.0. - score = float(points_earned) / float(points_possible) - - # There may be zero or more assignment records. We would expect for there - # to be zero if the user/course/usage combination does not relate to a - # previous graded LTI launch. This can happen if an LTI consumer embeds some - # gradable content in a context that doesn't require a score (maybe by - # including an exercise as a sample that students may complete but don't - # count towards their grade). - # There could be more than one GradedAssignment record if the same content - # is embedded more than once in a single course. This would be a strange - # course design on the consumer's part, but we handle it by sending update - # messages for all launches of the content. for assignment in assignments: - xml = lti_provider.outcomes.generate_replace_result_xml( - assignment.lis_result_sourcedid, score - ) - try: - response = lti_provider.outcomes.sign_and_send_replace_result(assignment, xml) - except RequestException: - # failed to send result. 'response' is None, so more detail will be - # logged at the end of the method. - response = None - log.exception("Outcome Service: Error when sending result.") + assignment.version_number += 1 + assignment.save() + return assignments - # If something went wrong, make sure that we have a complete log record. - # That way we can manually fix things up on the campus system later if - # necessary. - if not (response and lti_provider.outcomes.check_replace_result_response(response)): - log.error( - "Outcome Service: Failed to update score on LTI consumer. " - "User: %s, course: %s, usage: %s, score: %s, possible: %s " - "status: %s, body: %s", - user_id, - course_key, - usage_key, - points_earned, - points_possible, - response, - response.text if response else 'Unknown' - ) + +@CELERY_APP.task(name='lti_provider.tasks.send_composite_outcome') +def send_composite_outcome(user_id, course_id, assignment_id, version): + """ + Calculate and transmit the score for a composite module (such as a + vertical). + + A composite module may contain multiple problems, so we need to + calculate the total points earned and possible for all child problems. This + requires calculating the scores for the whole course, which is an expensive + operation. + + Callers should be aware that the score calculation code accesses the latest + scores from the database. This can lead to a race condition between a view + that updates a user's score and the calculation of the grade. If the Celery + task attempts to read the score from the database before the view exits (and + its transaction is committed), it will see a stale value. Care should be + taken that this task is not triggered until the view exits. + + The GradedAssignment model has a version_number field that is incremented + whenever the score is updated. It is used by this method for two purposes. + First, it allows the task to exit if it detects that it has been superseded + by another task that will transmit the score for the same assignment. + Second, it prevents a race condition where two tasks calculate different + scores for a single assignment, and may potentially update the campus LMS + in the wrong order. + """ + assignment = GradedAssignment.objects.get(id=assignment_id) + if version != assignment.version_number: + log.info( + "Score passback for GradedAssignment %s skipped. More recent score available.", + assignment.id + ) + return + course_key = CourseKey.from_string(course_id) + mapped_usage_key = assignment.usage_key.map_into_course(course_key) + user = User.objects.get(id=user_id) + course = modulestore().get_course(course_key, depth=0) + progress_summary = get_weighted_scores(user, course) + earned, possible = progress_summary.score_for_module(mapped_usage_key) + if possible == 0: + weighted_score = 0 + else: + weighted_score = float(earned) / float(possible) + + assignment = GradedAssignment.objects.get(id=assignment_id) + if assignment.version_number == version: + outcomes.send_score_update(assignment, weighted_score) + + +@CELERY_APP.task +def send_leaf_outcome(assignment_id, points_earned, points_possible): + """ + Calculate and transmit the score for a single problem. This method assumes + that the individual problem was the source of a score update, and so it + directly takes the points earned and possible values. As such it does not + have to calculate the scores for the course, making this method far faster + than send_outcome_for_composite_assignment. + """ + assignment = GradedAssignment.objects.get(id=assignment_id) + if points_possible == 0: + weighted_score = 0 + else: + weighted_score = float(points_earned) / float(points_possible) + outcomes.send_score_update(assignment, weighted_score) diff --git a/lms/djangoapps/lti_provider/tests/test_outcomes.py b/lms/djangoapps/lti_provider/tests/test_outcomes.py index 4845e7ff3e..e89e366d29 100644 --- a/lms/djangoapps/lti_provider/tests/test_outcomes.py +++ b/lms/djangoapps/lti_provider/tests/test_outcomes.py @@ -14,7 +14,8 @@ from student.tests.factories import UserFactory from lti_provider.models import GradedAssignment, LtiConsumer, OutcomeService import lti_provider.outcomes as outcomes -import lti_provider.tasks as tasks +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory, check_mongo_calls class StoreOutcomeParametersTest(TestCase): @@ -181,81 +182,6 @@ class SignAndSendReplaceResultTest(TestCase): self.assertEqual(response, 'response') -class SendOutcomeTest(TestCase): - """ - Tests for the send_outcome method in tasks.py - """ - - def setUp(self): - super(SendOutcomeTest, self).setUp() - self.course_key = CourseLocator( - org='some_org', - course='some_course', - run='some_run' - ) - self.usage_key = BlockUsageLocator( - course_key=self.course_key, - block_type='problem', - block_id='block_id' - ) - self.user = UserFactory.create() - self.points_possible = 10 - self.points_earned = 3 - self.generate_xml_mock = self.setup_patch( - 'lti_provider.outcomes.generate_replace_result_xml', - 'replace result XML' - ) - self.replace_result_mock = self.setup_patch( - 'lti_provider.outcomes.sign_and_send_replace_result', - 'replace result response' - ) - self.check_result_mock = self.setup_patch( - 'lti_provider.outcomes.check_replace_result_response', - True - ) - consumer = LtiConsumer( - consumer_name='Lti Consumer Name', - consumer_key='consumer_key', - consumer_secret='consumer_secret', - instance_guid='tool_instance_guid' - ) - consumer.save() - outcome = OutcomeService( - lis_outcome_service_url='http://example.com/service_url', - lti_consumer=consumer - ) - outcome.save() - self.assignment = GradedAssignment( - user=self.user, - course_key=self.course_key, - usage_key=self.usage_key, - outcome_service=outcome, - lis_result_sourcedid='sourcedid', - ) - self.assignment.save() - - def setup_patch(self, function_name, return_value): - """ - Patch a method with a given return value, and return the mock - """ - mock = MagicMock(return_value=return_value) - new_patch = patch(function_name, new=mock) - new_patch.start() - self.addCleanup(new_patch.stop) - return mock - - def test_send_outcome(self): - tasks.send_outcome( - self.points_possible, - self.points_earned, - self.user.id, - unicode(self.course_key), - unicode(self.usage_key) - ) - self.generate_xml_mock.assert_called_once_with('sourcedid', 0.3) - self.replace_result_mock.assert_called_once_with(self.assignment, 'replace result XML') - - class XmlHandlingTest(TestCase): """ Tests for the generate_replace_result_xml and check_replace_result_response @@ -408,3 +334,125 @@ class TestBodyHashClient(unittest.TestCase): ] for oauth_header in expected_oauth_headers: self.assertIn(oauth_header, prepped_req.headers['Authorization']) + + +class TestAssignmentsForProblem(ModuleStoreTestCase): + """ + Test cases for the assignments_for_problem method in outcomes.py + """ + def setUp(self): + super(TestAssignmentsForProblem, self).setUp() + self.user = UserFactory.create() + self.user_id = self.user.id + self.outcome_service = self.create_outcome_service('outcomes') + self.course = CourseFactory.create() + with self.store.bulk_operations(self.course.id, emit_signals=False): + self.chapter = ItemFactory.create(parent=self.course, category="chapter") + self.vertical = ItemFactory.create(parent=self.chapter, category="vertical") + self.unit = ItemFactory.create(parent=self.vertical, category="unit") + + def create_outcome_service(self, id_suffix): + """ + Create and save a new OutcomeService model in the test database. The + OutcomeService model requires an LtiConsumer model, so we create one of + those as well. The method takes an ID string that is used to ensure that + unique fields do not conflict. + """ + lti_consumer = LtiConsumer( + consumer_name='lti_consumer_name' + id_suffix, + consumer_key='lti_consumer_key' + id_suffix, + consumer_secret='lti_consumer_secret' + id_suffix, + instance_guid='lti_instance_guid' + id_suffix + ) + lti_consumer.save() + outcome_service = OutcomeService( + lis_outcome_service_url='https://example.com/outcomes/' + id_suffix, + lti_consumer=lti_consumer + ) + outcome_service.save() + return outcome_service + + def create_graded_assignment(self, desc, result_id, outcome_service): + """ + Create and save a new GradedAssignment model in the test database. + """ + assignment = GradedAssignment( + user=self.user, + course_key=self.course.id, + usage_key=desc.location, + outcome_service=outcome_service, + lis_result_sourcedid=result_id, + version_number=0 + ) + assignment.save() + return assignment + + def test_with_no_graded_assignments(self): + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 0) + + def test_with_graded_unit(self): + self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service) + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 1) + self.assertEqual(assignments[0].lis_result_sourcedid, 'graded_unit') + + def test_with_graded_vertical(self): + self.create_graded_assignment(self.vertical, 'graded_vertical', self.outcome_service) + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 1) + self.assertEqual(assignments[0].lis_result_sourcedid, 'graded_vertical') + + def test_with_graded_unit_and_vertical(self): + self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service) + self.create_graded_assignment(self.vertical, 'graded_vertical', self.outcome_service) + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 2) + self.assertEqual(assignments[0].lis_result_sourcedid, 'graded_unit') + self.assertEqual(assignments[1].lis_result_sourcedid, 'graded_vertical') + + def test_with_unit_used_twice(self): + self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service) + self.create_graded_assignment(self.unit, 'graded_unit2', self.outcome_service) + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 2) + self.assertEqual(assignments[0].lis_result_sourcedid, 'graded_unit') + self.assertEqual(assignments[1].lis_result_sourcedid, 'graded_unit2') + + def test_with_unit_graded_for_different_user(self): + self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service) + other_user = UserFactory.create() + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, other_user.id, self.course.id + ) + self.assertEqual(len(assignments), 0) + + def test_with_unit_graded_for_multiple_consumers(self): + other_outcome_service = self.create_outcome_service('second_consumer') + self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service) + self.create_graded_assignment(self.unit, 'graded_unit2', other_outcome_service) + with check_mongo_calls(3): + assignments = outcomes.get_assignments_for_problem( + self.unit, self.user_id, self.course.id + ) + self.assertEqual(len(assignments), 2) + self.assertEqual(assignments[0].lis_result_sourcedid, 'graded_unit') + self.assertEqual(assignments[1].lis_result_sourcedid, 'graded_unit2') + self.assertEqual(assignments[0].outcome_service, self.outcome_service) + self.assertEqual(assignments[1].outcome_service, other_outcome_service) diff --git a/lms/djangoapps/lti_provider/tests/test_tasks.py b/lms/djangoapps/lti_provider/tests/test_tasks.py new file mode 100644 index 0000000000..e181435d3d --- /dev/null +++ b/lms/djangoapps/lti_provider/tests/test_tasks.py @@ -0,0 +1,132 @@ +""" +Tests for the LTI outcome service handlers, both in outcomes.py and in tasks.py +""" + +import ddt +from django.test import TestCase +from mock import patch, MagicMock +from student.tests.factories import UserFactory + +from lti_provider.models import GradedAssignment, LtiConsumer, OutcomeService +import lti_provider.tasks as tasks +from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator + + +class BaseOutcomeTest(TestCase): + """ + Super type for tests of both the leaf and composite outcome celery tasks. + """ + def setUp(self): + super(BaseOutcomeTest, self).setUp() + self.course_key = CourseLocator( + org='some_org', + course='some_course', + run='some_run' + ) + self.usage_key = BlockUsageLocator( + course_key=self.course_key, + block_type='problem', + block_id='block_id' + ) + self.user = UserFactory.create() + self.consumer = LtiConsumer( + consumer_name='Lti Consumer Name', + consumer_key='consumer_key', + consumer_secret='consumer_secret', + instance_guid='tool_instance_guid' + ) + self.consumer.save() + outcome = OutcomeService( + lis_outcome_service_url='http://example.com/service_url', + lti_consumer=self.consumer + ) + outcome.save() + self.assignment = GradedAssignment( + user=self.user, + course_key=self.course_key, + usage_key=self.usage_key, + outcome_service=outcome, + lis_result_sourcedid='sourcedid', + version_number=1, + ) + self.assignment.save() + + self.send_score_update_mock = self.setup_patch( + 'lti_provider.outcomes.send_score_update', None + ) + + def setup_patch(self, function_name, return_value): + """ + Patch a method with a given return value, and return the mock + """ + mock = MagicMock(return_value=return_value) + new_patch = patch(function_name, new=mock) + new_patch.start() + self.addCleanup(new_patch.stop) + return mock + + +@ddt.ddt +class SendLeafOutcomeTest(BaseOutcomeTest): + """ + Tests for the send_leaf_outcome method in tasks.py + """ + @ddt.data( + (2.0, 2.0, 1.0), + (2.0, 0.0, 0.0), + (1, 2, 0.5), + ) + @ddt.unpack + def test_outcome_with_score(self, earned, possible, expected): + tasks.send_leaf_outcome( + self.assignment.id, # pylint: disable=no-member + earned, + possible + ) + self.send_score_update_mock.assert_called_once_with(self.assignment, expected) + + +@ddt.ddt +class SendCompositeOutcomeTest(BaseOutcomeTest): + """ + Tests for the send_composite_outcome method in tasks.py + """ + def setUp(self): + super(SendCompositeOutcomeTest, self).setUp() + self.descriptor = MagicMock() + self.descriptor.location = BlockUsageLocator( + course_key=self.course_key, + block_type='problem', + block_id='problem', + ) + self.weighted_scores = MagicMock() + self.weighted_scores_mock = self.setup_patch( + 'lti_provider.tasks.get_weighted_scores', self.weighted_scores + ) + self.module_store = MagicMock() + self.module_store.get_item = MagicMock(return_value=self.descriptor) + self.check_result_mock = self.setup_patch( + 'lti_provider.tasks.modulestore', + self.module_store + ) + + @ddt.data( + (2.0, 2.0, 1.0), + (2.0, 0.0, 0.0), + (1, 2, 0.5), + ) + @ddt.unpack + def test_outcome_with_score_score(self, earned, possible, expected): + self.weighted_scores.score_for_module = MagicMock(return_value=(earned, possible)) + tasks.send_composite_outcome( + self.user.id, unicode(self.course_key), self.assignment.id, 1 # pylint: disable=no-member + ) + self.send_score_update_mock.assert_called_once_with(self.assignment, expected) + + def test_outcome_with_outdated_version(self): + self.assignment.version_number = 2 + self.assignment.save() + tasks.send_composite_outcome( + self.user.id, unicode(self.course_key), self.assignment.id, 1 # pylint: disable=no-member + ) + self.assertEqual(self.weighted_scores_mock.call_count, 0) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index ef9251c20e..76b0b04e7b 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -25,7 +25,7 @@ from mock import patch, Mock import ddt from common.test.utils import XssTestMixin -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from student.roles import CourseSalesAdminRole from util.date_utils import get_default_time_display @@ -66,7 +66,21 @@ postpay_mock = Mock() @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) @ddt.ddt -class ShoppingCartViewsTests(ModuleStoreTestCase, XssTestMixin): +class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): + @classmethod + def setUpClass(cls): + super(ShoppingCartViewsTests, cls).setUpClass() + cls.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + cls.course_key = cls.course.id + + verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') + cls.verified_course_key = verified_course.id + + xss_course = CourseFactory.create(org='xssorg', number='test', display_name='<script>alert("XSS")</script>') + cls.xss_course_key = xss_course.id + + cls.testing_course = CourseFactory.create(org='edX', number='888', display_name='Testing Super Course') + def setUp(self): super(ShoppingCartViewsTests, self).setUp() @@ -80,8 +94,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase, XssTestMixin): self.coupon_code = 'abcde' self.reg_code = 'qwerty' self.percentage_discount = 10 - self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_key = self.course.id self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", @@ -90,19 +102,12 @@ class ShoppingCartViewsTests(ModuleStoreTestCase, XssTestMixin): # Saving another testing course mode self.testing_cost = 20 - self.testing_course = CourseFactory.create(org='edX', number='888', display_name='Testing Super Course') self.testing_course_mode = CourseMode(course_id=self.testing_course.id, mode_slug="honor", mode_display_name="testing honor cert", min_price=self.testing_cost) self.testing_course_mode.save() - verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') - self.verified_course_key = verified_course.id - - xss_course = CourseFactory.create(org='xssorg', number='test', display_name='<script>alert("XSS")</script>') - self.xss_course_key = xss_course.id - self.cart = Order.get_cart_for_user(self.user) self.addCleanup(patcher.stop) @@ -1360,20 +1365,71 @@ class ShoppingCartViewsTests(ModuleStoreTestCase, XssTestMixin): } ) + def test_shopping_cart_navigation_link_not_in_microsite(self): + """ + Tests shopping cart link is available in navigation header if request is not from a microsite. + """ + CourseEnrollment.enroll(self.user, self.course_key) + self.add_course_to_user_cart(self.testing_course.id) + resp = self.client.get(reverse('courseware', kwargs={'course_id': unicode(self.course.id)})) + self.assertEqual(resp.status_code, 200) + self.assertIn('<a class="shopping-cart"', resp.content) -class ReceiptRedirectTest(ModuleStoreTestCase): + def test_shopping_cart_navigation_link_not_in_microsite_and_not_on_courseware(self): + """ + Tests shopping cart link is available in navigation header if request is not from a microsite + and requested page is not courseware too. + """ + CourseEnrollment.enroll(self.user, self.course_key) + self.add_course_to_user_cart(self.testing_course.id) + resp = self.client.get(reverse('dashboard')) + self.assertEqual(resp.status_code, 200) + self.assertIn('<a class="shopping-cart"', resp.content) + + def test_shopping_cart_navigation_link_in_microsite_not_on_courseware(self): + """ + Tests shopping cart link is available in navigation header if request is from a microsite but requested + page is not from courseware. + """ + CourseEnrollment.enroll(self.user, self.course_key) + self.add_course_to_user_cart(self.testing_course.id) + with patch('microsite_configuration.microsite.is_request_in_microsite', + Mock(return_value=True)): + resp = self.client.get(reverse('dashboard')) + self.assertEqual(resp.status_code, 200) + self.assertIn('<a class="shopping-cart"', resp.content) + + def test_shopping_cart_navigation_link_in_microsite_courseware_page(self): + """ + Tests shopping cart link is not available in navigation header if request is from a microsite + and requested page is from courseware. + """ + CourseEnrollment.enroll(self.user, self.course_key) + self.add_course_to_user_cart(self.testing_course.id) + with patch('microsite_configuration.microsite.is_request_in_microsite', + Mock(return_value=True)): + resp = self.client.get(reverse('courseware', kwargs={'course_id': unicode(self.course.id)})) + self.assertEqual(resp.status_code, 200) + self.assertNotIn('<a class="shopping-cart"', resp.content) + + +class ReceiptRedirectTest(SharedModuleStoreTestCase): """Test special-case redirect from the receipt page. """ COST = 40 PASSWORD = 'password' + @classmethod + def setUpClass(cls): + super(ReceiptRedirectTest, cls).setUpClass() + cls.course = CourseFactory.create() + cls.course_key = cls.course.id + def setUp(self): super(ReceiptRedirectTest, self).setUp() self.user = UserFactory.create() self.user.set_password(self.PASSWORD) self.user.save() - self.course = CourseFactory.create() - self.course_key = self.course.id self.course_mode = CourseMode( course_id=self.course_key, mode_slug="verified", @@ -1382,7 +1438,6 @@ class ReceiptRedirectTest(ModuleStoreTestCase): ) self.course_mode.save() self.cart = Order.get_cart_for_user(self.user) - self.client.login( username=self.user.username, password=self.PASSWORD @@ -1429,7 +1484,6 @@ class ShoppingcartViewsClosedEnrollment(ModuleStoreTestCase): Test suite for ShoppingcartViews Course Enrollments Closed or not """ def setUp(self): - super(ShoppingcartViewsClosedEnrollment, self).setUp() self.user = UserFactory.create() self.user.set_password('password') @@ -1560,10 +1614,16 @@ class ShoppingcartViewsClosedEnrollment(ModuleStoreTestCase): @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) -class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): +class RegistrationCodeRedemptionCourseEnrollment(SharedModuleStoreTestCase): """ Test suite for RegistrationCodeRedemption Course Enrollments """ + @classmethod + def setUpClass(cls): + super(RegistrationCodeRedemptionCourseEnrollment, cls).setUpClass() + cls.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + cls.course_key = cls.course.id + def setUp(self, **kwargs): super(RegistrationCodeRedemptionCourseEnrollment, self).setUp() @@ -1571,8 +1631,6 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): self.user.set_password('password') self.user.save() self.cost = 40 - self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_key = self.course.id self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", @@ -1735,7 +1793,7 @@ class RedeemCodeEmbargoTests(UrlResetMixin, ModuleStoreTestCase): @ddt.ddt -class DonationViewTest(ModuleStoreTestCase): +class DonationViewTest(SharedModuleStoreTestCase): """Tests for making a donation. These tests cover both the single-item purchase flow, @@ -1745,6 +1803,11 @@ class DonationViewTest(ModuleStoreTestCase): DONATION_AMOUNT = "23.45" PASSWORD = "password" + @classmethod + def setUpClass(cls): + super(DonationViewTest, cls).setUpClass() + cls.course = CourseFactory.create(display_name="Test Course") + def setUp(self): """Create a test user and order. """ super(DonationViewTest, self).setUp() @@ -1766,8 +1829,7 @@ class DonationViewTest(ModuleStoreTestCase): self._assert_receipt_contains("tax purposes") def test_donation_for_course_receipt(self): - # Create a test course and donate to it - self.course = CourseFactory.create(display_name="Test Course") + # Donate to our course self._donate(self.DONATION_AMOUNT, course_id=self.course.id) # Verify the receipt page @@ -1891,10 +1953,18 @@ class DonationViewTest(ModuleStoreTestCase): return reverse("shoppingcart.views.show_receipt", kwargs={"ordernum": order_id}) -class CSVReportViewsTest(ModuleStoreTestCase): +class CSVReportViewsTest(SharedModuleStoreTestCase): """ Test suite for CSV Purchase Reporting """ + @classmethod + def setUpClass(cls): + super(CSVReportViewsTest, cls).setUpClass() + cls.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + cls.course_key = cls.course.id + verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') + cls.verified_course_key = verified_course.id + def setUp(self): super(CSVReportViewsTest, self).setUp() @@ -1902,8 +1972,6 @@ class CSVReportViewsTest(ModuleStoreTestCase): self.user.set_password('password') self.user.save() self.cost = 40 - self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_key = self.course.id self.course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", @@ -1914,9 +1982,7 @@ class CSVReportViewsTest(ModuleStoreTestCase): mode_display_name="verified cert", min_price=self.cost) self.course_mode2.save() - verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course') - self.verified_course_key = verified_course.id self.cart = Order.get_cart_for_user(self.user) self.dl_grp = Group(name=settings.PAYMENT_REPORT_GENERATOR_GROUP) self.dl_grp.save() diff --git a/lms/djangoapps/student_account/test/test_views.py b/lms/djangoapps/student_account/test/test_views.py index 6ade2ffda6..a845414562 100644 --- a/lms/djangoapps/student_account/test/test_views.py +++ b/lms/djangoapps/student_account/test/test_views.py @@ -217,8 +217,8 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi self.configure_facebook_provider(enabled=True) @ddt.data( - ("account_login", "login"), - ("account_register", "register"), + ("signin_user", "login"), + ("register_user", "register"), ) @ddt.unpack def test_login_and_registration_form(self, url_name, initial_mode): @@ -226,7 +226,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi expected_data = u"data-initial-mode=\"{mode}\"".format(mode=initial_mode) self.assertContains(response, expected_data) - @ddt.data("account_login", "account_register") + @ddt.data("signin_user", "register_user") def test_login_and_registration_form_already_authenticated(self, url_name): # Create/activate a new account and log in activation_key = create_account(self.USERNAME, self.PASSWORD, self.EMAIL) @@ -239,10 +239,10 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi self.assertRedirects(response, reverse("dashboard")) @ddt.data( - (False, "account_login"), - (False, "account_register"), - (True, "account_login"), - (True, "account_register"), + (False, "signin_user"), + (False, "register_user"), + (True, "signin_user"), + (True, "register_user"), ) @ddt.unpack def test_login_and_registration_form_signin_preserves_params(self, is_edx_domain, url_name): @@ -275,18 +275,18 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi self.assertContains(response, expected_url) @mock.patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) - @ddt.data("account_login", "account_register") + @ddt.data("signin_user", "register_user") def test_third_party_auth_disabled(self, url_name): response = self.client.get(reverse(url_name)) self._assert_third_party_auth_data(response, None, None, []) @ddt.data( - ("account_login", None, None), - ("account_register", None, None), - ("account_login", "google-oauth2", "Google"), - ("account_register", "google-oauth2", "Google"), - ("account_login", "facebook", "Facebook"), - ("account_register", "facebook", "Facebook"), + ("signin_user", None, None), + ("register_user", None, None), + ("signin_user", "google-oauth2", "Google"), + ("register_user", "google-oauth2", "Google"), + ("signin_user", "facebook", "Facebook"), + ("register_user", "facebook", "Facebook"), ) @ddt.unpack def test_third_party_auth(self, url_name, current_backend, current_provider): @@ -329,7 +329,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi def test_hinted_login(self): params = [("next", "/courses/something/?tpa_hint=oa2-google-oauth2")] - response = self.client.get(reverse('account_login'), params) + response = self.client.get(reverse('signin_user'), params) self.assertContains(response, "data-third-party-auth-hint='oa2-google-oauth2'") @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) @@ -337,7 +337,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi # Retrieve the login page from a microsite domain # and verify that we're served the old page. resp = self.client.get( - reverse("account_login"), + reverse("signin_user"), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertContains(resp, "Log into your Test Microsite Account") @@ -347,7 +347,7 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi # Retrieve the register page from a microsite domain # and verify that we're served the old page. resp = self.client.get( - reverse("account_register"), + reverse("register_user"), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertContains(resp, "Register for Test Microsite") @@ -474,7 +474,7 @@ class MicrositeLogistrationTests(TestCase): """ resp = self.client.get( - reverse('account_login'), + reverse('signin_user'), HTTP_HOST=settings.MICROSITE_LOGISTRATION_HOSTNAME ) self.assertEqual(resp.status_code, 200) @@ -488,7 +488,7 @@ class MicrositeLogistrationTests(TestCase): """ resp = self.client.get( - reverse('account_register'), + reverse('register_user'), HTTP_HOST=settings.MICROSITE_LOGISTRATION_HOSTNAME ) self.assertEqual(resp.status_code, 200) @@ -502,7 +502,7 @@ class MicrositeLogistrationTests(TestCase): """ resp = self.client.get( - reverse('account_login'), + reverse('signin_user'), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertEqual(resp.status_code, 200) @@ -510,7 +510,7 @@ class MicrositeLogistrationTests(TestCase): self.assertNotIn('<div id="login-and-registration-container"', resp.content) resp = self.client.get( - reverse('account_register'), + reverse('register_user'), HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME ) self.assertEqual(resp.status_code, 200) diff --git a/lms/djangoapps/student_account/urls.py b/lms/djangoapps/student_account/urls.py index 2792c46b82..9a7357d306 100644 --- a/lms/djangoapps/student_account/urls.py +++ b/lms/djangoapps/student_account/urls.py @@ -6,8 +6,6 @@ urlpatterns = [] if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'): urlpatterns += patterns( 'student_account.views', - url(r'^login/$', 'login_and_registration_form', {'initial_mode': 'login'}, name='account_login'), - url(r'^register/$', 'login_and_registration_form', {'initial_mode': 'register'}, name='account_register'), url(r'^password$', 'password_change_request_handler', name='password_change_request'), ) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index a2eda85848..706ffaa94b 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -102,6 +102,7 @@ def login_and_registration_form(request, initial_mode="login"): 'third_party_auth_hint': third_party_auth_hint or '', 'platform_name': settings.PLATFORM_NAME, 'responsive': True, + 'allow_iframing': True, # Include form descriptions retrieved from the user API. # We could have the JS client make these requests directly, @@ -187,7 +188,7 @@ def _third_party_auth_context(request, redirect_to): } if third_party_auth.is_enabled(): - for enabled in third_party_auth.provider.Registry.enabled(): + for enabled in third_party_auth.provider.Registry.accepting_logins(): info = { "id": enabled.provider_id, "name": enabled.name, @@ -208,12 +209,14 @@ def _third_party_auth_context(request, redirect_to): running_pipeline = pipeline.get(request) if running_pipeline is not None: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) - context["currentProvider"] = current_provider.name - context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) - if current_provider.skip_registration_form: - # As a reliable way of "skipping" the registration form, we just submit it automatically - context["autoSubmitRegForm"] = True + if current_provider is not None: + context["currentProvider"] = current_provider.name + context["finishAuthUrl"] = pipeline.get_complete_url(current_provider.backend_name) + + if current_provider.skip_registration_form: + # As a reliable way of "skipping" the registration form, we just submit it automatically + context["autoSubmitRegForm"] = True # Check for any error messages we may want to display: for msg in messages.get_messages(request): @@ -396,13 +399,14 @@ def account_settings_context(request): 'name': state.provider.name, # The name of the provider e.g. Facebook 'connected': state.has_account, # Whether the user's edX account is connected with the provider. # If the user is not connected, they should be directed to this page to authenticate - # with the particular provider. + # with the particular provider, as long as the provider supports initiating a login. 'connect_url': pipeline.get_login_url( state.provider.provider_id, pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS, # The url the user should be directed to after the auth process has completed. redirect_url=reverse('account_settings'), ), + 'accepts_logins': state.provider.accepts_logins, # If the user is connected, sending a POST request to this url removes the connection # information for this provider from their edX account. 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), diff --git a/lms/djangoapps/survey/migrations/0002_auto__add_field_surveyanswer_course_key.py b/lms/djangoapps/survey/migrations/0002_auto__add_field_surveyanswer_course_key.py new file mode 100644 index 0000000000..28ad94775e --- /dev/null +++ b/lms/djangoapps/survey/migrations/0002_auto__add_field_surveyanswer_course_key.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'SurveyAnswer.course_key' + db.add_column('survey_surveyanswer', 'course_key', + self.gf('xmodule_django.models.CourseKeyField')(max_length=255, null=True, db_index=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'SurveyAnswer.course_key' + db.delete_column('survey_surveyanswer', 'course_key') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'survey.surveyanswer': { + 'Meta': {'object_name': 'SurveyAnswer'}, + 'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'null': 'True', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'field_value': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'form': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['survey.SurveyForm']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'survey.surveyform': { + 'Meta': {'object_name': 'SurveyForm'}, + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'form': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + } + } + + complete_apps = ['survey'] \ No newline at end of file diff --git a/lms/djangoapps/survey/models.py b/lms/djangoapps/survey/models.py index 40e176bee4..88b9696364 100644 --- a/lms/djangoapps/survey/models.py +++ b/lms/djangoapps/survey/models.py @@ -13,6 +13,8 @@ from model_utils.models import TimeStampedModel from survey.exceptions import SurveyFormNameAlreadyExists, SurveyFormNotFound +from xmodule_django.models import CourseKeyField + log = logging.getLogger("edx.survey") @@ -104,7 +106,7 @@ class SurveyForm(TimeStampedModel): """ return SurveyAnswer.do_survey_answers_exist(self, user) - def save_user_answers(self, user, answers): + def save_user_answers(self, user, answers, course_key): """ Store answers to the form for a given user. Answers is a dict of simple name/value pairs @@ -112,7 +114,16 @@ class SurveyForm(TimeStampedModel): IMPORTANT: There is no validaton of form answers at this point. All data supplied to this method is presumed to be previously validated """ - SurveyAnswer.save_answers(self, user, answers) + + # first remove any answer the user might have done before + self.clear_user_answers(user) + SurveyAnswer.save_answers(self, user, answers, course_key) + + def clear_user_answers(self, user): + """ + Removes all answers that a user has submitted + """ + SurveyAnswer.objects.filter(form=self, user=user).delete() def get_field_names(self): """ @@ -135,7 +146,10 @@ class SurveyForm(TimeStampedModel): # NOTE: This wrapping doesn't change the ability to query it tree = etree.fromstring(u'<div>{}</div>'.format(html)) - input_fields = tree.findall('.//input') + tree.findall('.//select') + input_fields = ( + tree.findall('.//input') + tree.findall('.//select') + + tree.findall('.//textarea') + ) for input_field in input_fields: if 'name' in input_field.keys() and input_field.attrib['name'] not in names: @@ -153,6 +167,10 @@ class SurveyAnswer(TimeStampedModel): field_name = models.CharField(max_length=255, db_index=True) field_value = models.CharField(max_length=1024) + # adding the course_id where the end-user answered the survey question + # since it didn't exist in the beginning, it is nullable + course_key = CourseKeyField(max_length=255, db_index=True, null=True) + @classmethod def do_survey_answers_exist(cls, form, user): """ @@ -205,7 +223,7 @@ class SurveyAnswer(TimeStampedModel): return results @classmethod - def save_answers(cls, form, user, answers): + def save_answers(cls, form, user, answers, course_key): """ Store answers to the form for a given user. Answers is a dict of simple name/value pairs @@ -219,6 +237,20 @@ class SurveyAnswer(TimeStampedModel): # See if there is an answer stored for this user, form, field_name pair or not # this will allow for update cases. This does include an additional lookup, # but write operations will be relatively infrequent - answer, __ = SurveyAnswer.objects.get_or_create(user=user, form=form, field_name=name) - answer.field_value = value - answer.save() + value = answers[name] + defaults = {"field_value": value} + if course_key: + defaults['course_key'] = course_key + + answer, created = SurveyAnswer.objects.get_or_create( + user=user, + form=form, + field_name=name, + defaults=defaults + ) + + if not created: + # Allow for update cases. + answer.field_value = value + answer.course_key = course_key + answer.save() diff --git a/lms/djangoapps/survey/tests/test_models.py b/lms/djangoapps/survey/tests/test_models.py index 370fe53184..8edbea5d16 100644 --- a/lms/djangoapps/survey/tests/test_models.py +++ b/lms/djangoapps/survey/tests/test_models.py @@ -2,6 +2,7 @@ Python tests for the Survey models """ +import ddt from collections import OrderedDict from django.test import TestCase @@ -10,9 +11,10 @@ from django.contrib.auth.models import User from survey.exceptions import SurveyFormNotFound, SurveyFormNameAlreadyExists from django.core.exceptions import ValidationError -from survey.models import SurveyForm +from survey.models import SurveyForm, SurveyAnswer +@ddt.ddt class SurveyModelsTests(TestCase): """ All tests for the Survey models.py file @@ -32,12 +34,22 @@ class SurveyModelsTests(TestCase): self.test_survey_name = 'TestForm' self.test_form = '<li><input name="field1" /></li><li><input name="field2" /></li><li><select name="ddl"><option>1</option></select></li>' self.test_form_update = '<input name="field1" />' + self.course_id = 'foo/bar/baz' self.student_answers = OrderedDict({ 'field1': 'value1', 'field2': 'value2', }) + self.student_answers_update = OrderedDict({ + 'field1': 'value1-updated', + 'field2': 'value2-updated', + }) + + self.student_answers_update2 = OrderedDict({ + 'field1': 'value1-updated2', + }) + self.student2_answers = OrderedDict({ 'field1': 'value3' }) @@ -142,7 +154,8 @@ class SurveyModelsTests(TestCase): self.assertFalse(survey.has_user_answered_survey(self.student)) self.assertEquals(len(survey.get_answers()), 0) - def test_single_user_answers(self): + @ddt.data(None, 'foo/bar/baz') + def test_single_user_answers(self, course_id): """ Create a new survey and add answers to it """ @@ -150,7 +163,7 @@ class SurveyModelsTests(TestCase): survey = self._create_test_survey() self.assertIsNotNone(survey) - survey.save_user_answers(self.student, self.student_answers) + survey.save_user_answers(self.student, self.student_answers, course_id) self.assertTrue(survey.has_user_answered_survey(self.student)) @@ -164,6 +177,19 @@ class SurveyModelsTests(TestCase): self.assertTrue(self.student.id in answers) self.assertEquals(all_answers[self.student.id], self.student_answers) + # check that the course_id was set + + answer_objs = SurveyAnswer.objects.filter( + user=self.student, + form=survey + ) + + for answer_obj in answer_objs: + if course_id: + self.assertEquals(unicode(answer_obj.course_key), course_id) + else: + self.assertIsNone(answer_obj.course_key) + def test_multiple_user_answers(self): """ Create a new survey and add answers to it @@ -172,8 +198,8 @@ class SurveyModelsTests(TestCase): survey = self._create_test_survey() self.assertIsNotNone(survey) - survey.save_user_answers(self.student, self.student_answers) - survey.save_user_answers(self.student2, self.student2_answers) + survey.save_user_answers(self.student, self.student_answers, self.course_id) + survey.save_user_answers(self.student2, self.student2_answers, self.course_id) self.assertTrue(survey.has_user_answered_survey(self.student)) @@ -187,12 +213,43 @@ class SurveyModelsTests(TestCase): answers = survey.get_answers(self.student) self.assertEquals(len(answers.keys()), 1) self.assertTrue(self.student.id in answers) - self.assertEquals(all_answers[self.student.id], self.student_answers) + self.assertEquals(answers[self.student.id], self.student_answers) answers = survey.get_answers(self.student2) self.assertEquals(len(answers.keys()), 1) self.assertTrue(self.student2.id in answers) - self.assertEquals(all_answers[self.student2.id], self.student2_answers) + self.assertEquals(answers[self.student2.id], self.student2_answers) + + def test_update_answers(self): + """ + Make sure the update case works + """ + + survey = self._create_test_survey() + self.assertIsNotNone(survey) + + survey.save_user_answers(self.student, self.student_answers, self.course_id) + + answers = survey.get_answers(self.student) + self.assertEquals(len(answers.keys()), 1) + self.assertTrue(self.student.id in answers) + self.assertEquals(answers[self.student.id], self.student_answers) + + # update + survey.save_user_answers(self.student, self.student_answers_update, self.course_id) + + answers = survey.get_answers(self.student) + self.assertEquals(len(answers.keys()), 1) + self.assertTrue(self.student.id in answers) + self.assertEquals(answers[self.student.id], self.student_answers_update) + + # update with just a subset of the origin dataset + survey.save_user_answers(self.student, self.student_answers_update2, self.course_id) + + answers = survey.get_answers(self.student) + self.assertEquals(len(answers.keys()), 1) + self.assertTrue(self.student.id in answers) + self.assertEquals(answers[self.student.id], self.student_answers_update2) def test_limit_num_users(self): """ @@ -201,8 +258,8 @@ class SurveyModelsTests(TestCase): """ survey = self._create_test_survey() - survey.save_user_answers(self.student, self.student_answers) - survey.save_user_answers(self.student2, self.student2_answers) + survey.save_user_answers(self.student, self.student_answers, self.course_id) + survey.save_user_answers(self.student2, self.student2_answers, self.course_id) # even though we have 2 users submitted answers # limit the result set to just 1 @@ -217,8 +274,8 @@ class SurveyModelsTests(TestCase): survey = self._create_test_survey() self.assertIsNotNone(survey) - survey.save_user_answers(self.student, self.student_answers) - survey.save_user_answers(self.student2, self.student2_answers) + survey.save_user_answers(self.student, self.student_answers, self.course_id) + survey.save_user_answers(self.student2, self.student2_answers, self.course_id) names = survey.get_field_names() diff --git a/lms/djangoapps/survey/tests/test_utils.py b/lms/djangoapps/survey/tests/test_utils.py index 320a4ad93c..00c1d3ea41 100644 --- a/lms/djangoapps/survey/tests/test_utils.py +++ b/lms/djangoapps/survey/tests/test_utils.py @@ -108,7 +108,7 @@ class SurveyModelsTests(ModuleStoreTestCase): """ Assert that a new course which has a required survey and user has answers for it """ - self.survey.save_user_answers(self.student, self.student_answers) + self.survey.save_user_answers(self.student, self.student_answers, None) self.assertFalse(must_answer_survey(self.course, self.student)) def test_staff_must_answer_survey(self): diff --git a/lms/djangoapps/survey/tests/test_views.py b/lms/djangoapps/survey/tests/test_views.py index 2d1d718e87..66f4c3f0c7 100644 --- a/lms/djangoapps/survey/tests/test_views.py +++ b/lms/djangoapps/survey/tests/test_views.py @@ -9,7 +9,7 @@ from django.test.client import Client from django.contrib.auth.models import User from django.core.urlresolvers import reverse -from survey.models import SurveyForm +from survey.models import SurveyForm, SurveyAnswer from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -32,15 +32,20 @@ class SurveyViewsTests(ModuleStoreTestCase): self.student = User.objects.create_user('student', 'student@test.com', self.password) self.test_survey_name = 'TestSurvey' - self.test_form = '<input name="field1" /><input name="field2" /><select name="ddl"><option>1</option></select>' + self.test_form = ''' + <input name="field1" /><input name="field2" /><select name="ddl"><option>1</option></select> + <textarea name="textarea" /> + ''' self.student_answers = OrderedDict({ u'field1': u'value1', u'field2': u'value2', u'ddl': u'1', + u'textarea': u'textarea' }) self.course = CourseFactory.create( + display_name='Test Course', course_survey_required=True, course_survey_name=self.test_survey_name ) @@ -124,6 +129,7 @@ class SurveyViewsTests(ModuleStoreTestCase): data['csrfmiddlewaretoken'] = 'foo' data['_redirect_url'] = 'bar' + data['course_id'] = unicode(self.course.id) resp = self.client.post( self.postback_url, @@ -133,6 +139,16 @@ class SurveyViewsTests(ModuleStoreTestCase): answers = self.survey.get_answers(self.student) self.assertNotIn('csrfmiddlewaretoken', answers[self.student.id]) self.assertNotIn('_redirect_url', answers[self.student.id]) + self.assertNotIn('course_id', answers[self.student.id]) + + # however we want to make sure we persist the course_id + answer_objs = SurveyAnswer.objects.filter( + user=self.student, + form=self.survey + ) + + for answer_obj in answer_objs: + self.assertEquals(unicode(answer_obj.course_key), data['course_id']) def test_encoding_answers(self): """ diff --git a/lms/djangoapps/survey/views.py b/lms/djangoapps/survey/views.py index be9c547443..6eee188313 100644 --- a/lms/djangoapps/survey/views.py +++ b/lms/djangoapps/survey/views.py @@ -14,6 +14,8 @@ from django.views.decorators.http import require_POST from django.conf import settings from django.utils.html import escape +from opaque_keys.edx.keys import CourseKey + from edxmako.shortcuts import render_to_response from survey.models import SurveyForm from microsite_configuration import microsite @@ -92,6 +94,8 @@ def submit_answers(request, survey_name): # in a hidden form field redirect_url = answers['_redirect_url'] if '_redirect_url' in answers else reverse('dashboard') + course_key = CourseKey.from_string(answers['course_id']) if 'course_id' in answers else None + allowed_field_names = survey.get_field_names() # scrub the answers to make sure nothing malicious from the user gets stored in @@ -102,7 +106,7 @@ def submit_answers(request, survey_name): if answer_key in allowed_field_names: filtered_answers[answer_key] = escape(answers[answer_key]) - survey.save_user_answers(request.user, filtered_answers) + survey.save_user_answers(request.user, filtered_answers, course_key) response_params = json.dumps({ # The HTTP end-point for the payment processor. diff --git a/lms/djangoapps/teams/errors.py b/lms/djangoapps/teams/errors.py index 36b8ca54a0..a011ea6780 100644 --- a/lms/djangoapps/teams/errors.py +++ b/lms/djangoapps/teams/errors.py @@ -14,3 +14,13 @@ class NotEnrolledInCourseForTeam(TeamAPIRequestError): class AlreadyOnTeamInCourse(TeamAPIRequestError): """User is already a member of another team in the same course.""" pass + + +class ElasticSearchConnectionError(TeamAPIRequestError): + """System was unable to connect to the configured elasticsearch instance""" + pass + + +class ImmutableMembershipFieldException(Exception): + """An attempt was made to change an immutable field on a CourseTeamMembership model""" + pass diff --git a/lms/djangoapps/teams/management/commands/reindex_course_team.py b/lms/djangoapps/teams/management/commands/reindex_course_team.py index 7bf21a9f2f..f857736a64 100644 --- a/lms/djangoapps/teams/management/commands/reindex_course_team.py +++ b/lms/djangoapps/teams/management/commands/reindex_course_team.py @@ -54,7 +54,7 @@ class Command(BaseCommand): if len(args) == 0 and not options.get('all', False): raise CommandError(u"reindex_course_team requires one or more arguments: <course_team_id>") elif not settings.FEATURES.get('ENABLE_TEAMS_SEARCH', False): - raise CommandError(u"ENABLE_TEAMS_SEARCH must be enabled") + raise CommandError(u"ENABLE_TEAMS_SEARCH must be enabled to use course team indexing") if options.get('all', False): course_teams = CourseTeam.objects.all() diff --git a/lms/djangoapps/teams/migrations/0005_add_course_id_and_topic_id_composite_index.py b/lms/djangoapps/teams/migrations/0005_add_course_id_and_topic_id_composite_index.py new file mode 100644 index 0000000000..eedd170cce --- /dev/null +++ b/lms/djangoapps/teams/migrations/0005_add_course_id_and_topic_id_composite_index.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +import pytz +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Create a composite index of course_id and topic_id. + db.create_index('teams_courseteam', ['course_id', 'topic_id']) + + def backwards(self, orm): + # Delete the composite index of course_id and topic_id. + db.delete_index('teams_courseteam', ['course_id', 'topic_id']) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'teams.courseteam': { + 'Meta': {'object_name': 'CourseTeam'}, + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'discussion_topic_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'language': ('student.models.LanguageField', [], {'max_length': '16', 'blank': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'team_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'topic_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'teams'", 'symmetrical': 'False', 'through': "orm['teams.CourseTeamMembership']", 'to': "orm['auth.User']"}) + }, + 'teams.courseteammembership': { + 'Meta': {'unique_together': "(('user', 'team'),)", 'object_name': 'CourseTeamMembership'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'membership'", 'to': "orm['teams.CourseTeam']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['teams'] diff --git a/lms/djangoapps/teams/migrations/0006_add_team_size.py b/lms/djangoapps/teams/migrations/0006_add_team_size.py new file mode 100644 index 0000000000..a89c03895c --- /dev/null +++ b/lms/djangoapps/teams/migrations/0006_add_team_size.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +from teams.models import CourseTeamMembership + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'CourseTeam.team_size' + db.add_column('teams_courseteam', 'team_size', + self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True), + keep_default=False) + + # Adding index on 'CourseTeam', fields ['last_activity_at'] + db.create_index('teams_courseteam', ['last_activity_at']) + + if not db.dry_run: + for team in orm.CourseTeam.objects.all(): + team.team_size = CourseTeamMembership.objects.filter(team=team).count() + team.save() + + def backwards(self, orm): + # Removing index on 'CourseTeam', fields ['last_activity_at'] + db.delete_index('teams_courseteam', ['last_activity_at']) + + # Deleting field 'CourseTeam.team_size' + db.delete_column('teams_courseteam', 'team_size') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'teams.courseteam': { + 'Meta': {'object_name': 'CourseTeam'}, + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'discussion_topic_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'language': ('student.models.LanguageField', [], {'max_length': '16', 'blank': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'team_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'team_size': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'topic_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'teams'", 'symmetrical': 'False', 'through': "orm['teams.CourseTeamMembership']", 'to': "orm['auth.User']"}) + }, + 'teams.courseteammembership': { + 'Meta': {'unique_together': "(('user', 'team'),)", 'object_name': 'CourseTeamMembership'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'membership'", 'to': "orm['teams.CourseTeam']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['teams'] diff --git a/lms/djangoapps/teams/migrations/0007_auto__del_field_courseteam_is_active.py b/lms/djangoapps/teams/migrations/0007_auto__del_field_courseteam_is_active.py new file mode 100644 index 0000000000..1810468dbe --- /dev/null +++ b/lms/djangoapps/teams/migrations/0007_auto__del_field_courseteam_is_active.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'CourseTeam.is_active' + db.delete_column('teams_courseteam', 'is_active') + + + def backwards(self, orm): + # Adding field 'CourseTeam.is_active' + db.add_column('teams_courseteam', 'is_active', + self.gf('django.db.models.fields.BooleanField')(default=True), + keep_default=False) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'teams.courseteam': { + 'Meta': {'object_name': 'CourseTeam'}, + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'discussion_topic_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('student.models.LanguageField', [], {'max_length': '16', 'blank': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'team_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'team_size': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True'}), + 'topic_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'related_name': "'teams'", 'symmetrical': 'False', 'through': "orm['teams.CourseTeamMembership']", 'to': "orm['auth.User']"}) + }, + 'teams.courseteammembership': { + 'Meta': {'unique_together': "(('user', 'team'),)", 'object_name': 'CourseTeamMembership'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'membership'", 'to': "orm['teams.CourseTeam']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['teams'] \ No newline at end of file diff --git a/lms/djangoapps/teams/models.py b/lms/djangoapps/teams/models.py index 8cba347235..e94e76da26 100644 --- a/lms/djangoapps/teams/models.py +++ b/lms/djangoapps/teams/models.py @@ -26,7 +26,7 @@ from django_comment_common.signals import ( from xmodule_django.models import CourseKeyField from util.model_utils import slugify from student.models import LanguageField, CourseEnrollment -from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam +from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam, ImmutableMembershipFieldException from teams import TEAM_DISCUSSION_CONTEXT @@ -76,7 +76,6 @@ class CourseTeam(models.Model): team_id = models.CharField(max_length=255, unique=True) discussion_topic_id = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, db_index=True) - is_active = models.BooleanField(default=True) course_id = CourseKeyField(max_length=255, db_index=True) topic_id = models.CharField(max_length=255, db_index=True, blank=True) date_created = models.DateTimeField(auto_now_add=True) @@ -86,8 +85,9 @@ class CourseTeam(models.Model): blank=True, help_text=ugettext_lazy("Optional language the team uses as ISO 639-1 code."), ) - last_activity_at = models.DateTimeField() + last_activity_at = models.DateTimeField(db_index=True) # indexed for ordering users = models.ManyToManyField(User, db_index=True, related_name='teams', through='CourseTeamMembership') + team_size = models.IntegerField(default=0, db_index=True) # indexed for ordering @classmethod def create(cls, name, course_id, description, topic_id=None, country=None, language=None): @@ -135,6 +135,11 @@ class CourseTeam(models.Model): team=self ) + def reset_team_size(self): + """Reset team_size to reflect the current membership count.""" + self.team_size = CourseTeamMembership.objects.filter(team=self).count() + self.save() + class CourseTeamMembership(models.Model): """This model represents the membership of a single user in a single team.""" @@ -148,12 +153,40 @@ class CourseTeamMembership(models.Model): date_joined = models.DateTimeField(auto_now_add=True) last_activity_at = models.DateTimeField() + immutable_fields = ('user', 'team', 'date_joined') + + def __setattr__(self, name, value): + """Memberships are immutable, with the exception of last activity + date. + """ + if name in self.immutable_fields: + # Check the current value -- if it is None, then this + # model is being created from the database and it's fine + # to set the value. Otherwise, we're trying to overwrite + # an immutable field. + current_value = getattr(self, name, None) + if current_value is not None: + raise ImmutableMembershipFieldException + super(CourseTeamMembership, self).__setattr__(name, value) + def save(self, *args, **kwargs): - """ Customize save method to set the last_activity_at if it does not currently exist. """ + """Customize save method to set the last_activity_at if it does not + currently exist. Also resets the team's size if this model is + being created. + """ + should_reset_team_size = False + if self.pk is None: + should_reset_team_size = True if not self.last_activity_at: self.last_activity_at = datetime.utcnow().replace(tzinfo=pytz.utc) - super(CourseTeamMembership, self).save(*args, **kwargs) + if should_reset_team_size: + self.team.reset_team_size() # pylint: disable=no-member + + def delete(self, *args, **kwargs): + """Recompute the related team's team_size after deleting a membership""" + super(CourseTeamMembership, self).delete(*args, **kwargs) + self.team.reset_team_size() # pylint: disable=no-member @classmethod def get_memberships(cls, username=None, course_ids=None, team_id=None): diff --git a/lms/djangoapps/teams/search_indexes.py b/lms/djangoapps/teams/search_indexes.py index 22af7227d4..090f3856ce 100644 --- a/lms/djangoapps/teams/search_indexes.py +++ b/lms/djangoapps/teams/search_indexes.py @@ -1,11 +1,15 @@ """ Search index used to load data into elasticsearch""" +import logging +from requests import ConnectionError + from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver from search.search_engine_base import SearchEngine +from .errors import ElasticSearchConnectionError from .serializers import CourseTeamSerializer, CourseTeam @@ -78,7 +82,11 @@ class CourseTeamIndexer(object): Return course team search engine (if feature is enabled). """ if cls.search_is_enabled(): - return SearchEngine.get_search_engine(index=cls.INDEX_NAME) + try: + return SearchEngine.get_search_engine(index=cls.INDEX_NAME) + except ConnectionError as err: + logging.error("Error connecting to elasticsearch: %s", err) + raise ElasticSearchConnectionError @classmethod def search_is_enabled(cls): @@ -93,4 +101,7 @@ def course_team_post_save_callback(**kwargs): """ Reindex object after save. """ - CourseTeamIndexer.index(kwargs['instance']) + try: + CourseTeamIndexer.index(kwargs['instance']) + except ElasticSearchConnectionError: + pass diff --git a/lms/djangoapps/teams/serializers.py b/lms/djangoapps/teams/serializers.py index 7f403c2957..b33d373287 100644 --- a/lms/djangoapps/teams/serializers.py +++ b/lms/djangoapps/teams/serializers.py @@ -35,8 +35,8 @@ class UserMembershipSerializer(serializers.ModelSerializer): class Meta(object): """Defines meta information for the ModelSerializer.""" model = CourseTeamMembership - fields = ("user", "date_joined") - read_only_fields = ("date_joined",) + fields = ("user", "date_joined", "last_activity_at") + read_only_fields = ("date_joined", "last_activity_at") class CourseTeamSerializer(serializers.ModelSerializer): @@ -51,7 +51,6 @@ class CourseTeamSerializer(serializers.ModelSerializer): "id", "discussion_topic_id", "name", - "is_active", "course_id", "topic_id", "date_created", diff --git a/lms/djangoapps/teams/static/teams/js/collections/base.js b/lms/djangoapps/teams/static/teams/js/collections/base.js index 01410af938..9a11592a3b 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/base.js +++ b/lms/djangoapps/teams/static/teams/js/collections/base.js @@ -11,31 +11,11 @@ this.teamEvents = options.teamEvents; this.teamEvents.bind('teams:update', this.onUpdate, this); - this.isStale = false; }, onUpdate: function(event) { + // Mark the collection as stale so that it knows to refresh when needed. this.isStale = true; - }, - - /** - * Refreshes the collection if it has been marked as stale. - * @param force If true, it will always refresh. - * @returns {promise} Returns a promise representing the refresh - */ - refresh: function(force) { - var self = this, - deferred = $.Deferred(); - if (force || this.isStale) { - this.setPage(1) - .done(function() { - self.isStale = false; - deferred.resolve(); - }); - } else { - deferred.resolve(); - } - return deferred.promise(); } }); return BaseCollection; diff --git a/lms/djangoapps/teams/static/teams/js/collections/team.js b/lms/djangoapps/teams/static/teams/js/collections/team.js index 417f7cdf4e..3b0fac856a 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team.js @@ -3,6 +3,8 @@ define(['teams/js/collections/base', 'teams/js/models/team', 'gettext'], function(BaseCollection, TeamModel, gettext) { var TeamCollection = BaseCollection.extend({ + sortField: 'last_activity_at', + initialize: function(teams, options) { var self = this; BaseCollection.prototype.initialize.call(this, options); @@ -12,14 +14,14 @@ topic_id: this.topic_id = options.topic_id, expand: 'user', course_id: function () { return encodeURIComponent(self.course_id); }, - order_by: function () { return 'name'; } // TODO surface sort order in UI + order_by: function () { return self.searchString ? '' : this.sortField; } }, BaseCollection.prototype.server_api ); delete this.server_api.sort_order; // Sort order is not specified for the Team API - this.registerSortableField('name', gettext('name')); - this.registerSortableField('open_slots', gettext('open_slots')); + this.registerSortableField('last_activity_at', gettext('last activity')); + this.registerSortableField('open_slots', gettext('open slots')); }, model: TeamModel diff --git a/lms/djangoapps/teams/static/teams/js/collections/team_membership.js b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js index bfb29bffc4..77f6fa4aa0 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/team_membership.js +++ b/lms/djangoapps/teams/static/teams/js/collections/team_membership.js @@ -14,7 +14,7 @@ this.server_api = _.extend( { - expand: 'team', + expand: 'team,user', username: this.username, course_id: function () { return encodeURIComponent(self.course_id); } }, diff --git a/lms/djangoapps/teams/static/teams/js/collections/topic.js b/lms/djangoapps/teams/static/teams/js/collections/topic.js index b3b60849ec..b88392a838 100644 --- a/lms/djangoapps/teams/static/teams/js/collections/topic.js +++ b/lms/djangoapps/teams/static/teams/js/collections/topic.js @@ -25,7 +25,9 @@ }, onUpdate: function(event) { - this.isStale = this.isStale || event.action === 'create'; + if (event.action === 'create') { + this.isStale = true; + } }, model: TopicModel diff --git a/lms/djangoapps/teams/static/teams/js/models/team.js b/lms/djangoapps/teams/static/teams/js/models/team.js index f8771f6e80..3ee13b53ae 100644 --- a/lms/djangoapps/teams/static/teams/js/models/team.js +++ b/lms/djangoapps/teams/static/teams/js/models/team.js @@ -8,7 +8,6 @@ defaults: { id: null, name: '', - is_active: null, course_id: '', topic_id: '', date_created: '', diff --git a/lms/djangoapps/teams/static/teams/js/models/team_membership.js b/lms/djangoapps/teams/static/teams/js/models/team_membership.js index 7bd5cfdc4a..ce9963fede 100644 --- a/lms/djangoapps/teams/static/teams/js/models/team_membership.js +++ b/lms/djangoapps/teams/static/teams/js/models/team_membership.js @@ -7,11 +7,12 @@ var TeamMembership = Backbone.Model.extend({ defaults: { date_joined: '', + last_activity_at: '', team: null, user: null }, - parse: function (response, options) { + parse: function (response) { response.team = new TeamModel(response.team); return response; } diff --git a/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js b/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js index ee929edf69..da43b583d6 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/collections/topic_collection_spec.js @@ -28,7 +28,7 @@ define(['backbone', 'URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', }); it('passes a course_id to the server', function () { - testRequestParam(this, 'course_id', 'my/course/id'); + testRequestParam(this, 'course_id', TeamSpecHelpers.testCourseID); }); it('URL encodes its course_id ', function () { diff --git a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js index cab815b09a..c32a0827b5 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/teams_tab_factory_spec.js @@ -1,24 +1,13 @@ -define(["jquery", "backbone", "teams/js/teams_tab_factory"], - function($, Backbone, TeamsTabFactory) { +define(['jquery', 'backbone', 'teams/js/teams_tab_factory', + 'teams/js/spec_helpers/team_spec_helpers'], + function($, Backbone, TeamsTabFactory, TeamSpecHelpers) { 'use strict'; - + describe("Teams Tab Factory", function() { var teamsTab; var initializeTeamsTabFactory = function() { - TeamsTabFactory({ - topics: {results: []}, - topicsUrl: '', - teamsUrl: '', - maxTeamSize: 9999, - courseID: 'edX/DemoX/Demo_Course', - userInfo: { - username: 'test-user', - privileged: false, - staff: false, - team_memberships_data: null - } - }); + TeamsTabFactory(TeamSpecHelpers.createMockContext()); }; beforeEach(function() { diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js index 5a68d3163b..843c5ce186 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/edit_team_spec.js @@ -13,22 +13,21 @@ define([ var teamsUrl = '/api/team/v0/teams/', createTeamData = { id: null, - name: "TeamName", - is_active: null, - course_id: "a/b/c", - topic_id: "awesomeness", - date_created: "", - description: "TeamDescription", - country: "US", - language: "en", + name: 'TeamName', + course_id: TeamSpecHelpers.testCourseID, + topic_id: TeamSpecHelpers.testTopicID, + date_created: '', + description: 'TeamDescription', + country: 'US', + language: 'en', membership: [], last_activity_at: '' }, editTeamData = { - name: "UpdatedAvengers", - description: "We do not discuss about avengers.", - country: "US", - language: "en" + name: 'UpdatedAvengers', + description: 'We do not discuss about avengers.', + country: 'US', + language: 'en' }, verifyValidation = function (requests, teamEditView, fieldsData) { _.each(fieldsData, function (fieldData) { @@ -39,17 +38,19 @@ define([ var message = teamEditView.$('.wrapper-msg'); expect(message.hasClass('is-hidden')).toBeFalsy(); - var actionMessage = (teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.'); + var actionMessage = ( + teamAction === 'create' ? 'Your team could not be created.' : 'Your team could not be updated.' + ); expect(message.find('.title').text().trim()).toBe(actionMessage); expect(message.find('.copy').text().trim()).toBe( - "Check the highlighted fields below and try again." + 'Check the highlighted fields below and try again.' ); _.each(fieldsData, function (fieldData) { if (fieldData[2] === 'error') { - expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(1); + expect(teamEditView.$(fieldData[0].split(' ')[0] + '.error').length).toBe(1); } else if (fieldData[2] === 'success') { - expect(teamEditView.$(fieldData[0].split(" ")[0] + '.error').length).toBe(0); + expect(teamEditView.$(fieldData[0].split(' ')[0] + '.error').length).toBe(0); } }); @@ -59,9 +60,9 @@ define([ teamAction; var createEditTeamView = function () { - var teamModel = {}; + var testTeam = {}; if (teamAction === 'edit') { - teamModel = new TeamModel( + testTeam = new TeamModel( { id: editTeamID, name: 'Avengers', @@ -81,16 +82,9 @@ define([ teamEvents: TeamSpecHelpers.teamEvents, el: $('.teams-content'), action: teamAction, - model: teamModel, - teamParams: { - teamsUrl: teamsUrl, - courseID: "a/b/c", - topicID: 'awesomeness', - topicName: 'Awesomeness', - languages: [['aa', 'Afar'], ['fr', 'French'], ['en', 'English']], - countries: [['af', 'Afghanistan'], ['CA', 'Canada'], ['US', 'United States']], - teamsDetailUrl: teamModel.url - } + model: testTeam, + topic: TeamSpecHelpers.createMockTopic(), + context: TeamSpecHelpers.testContext }).render(); }; @@ -134,13 +128,13 @@ define([ teamEditView.$('.u-field-name input').val(teamsData.name); teamEditView.$('.u-field-textarea textarea').val(teamsData.description); - teamEditView.$('.u-field-language select').val(teamsData.language).attr("selected", "selected"); - teamEditView.$('.u-field-country select').val(teamsData.country).attr("selected", "selected"); + teamEditView.$('.u-field-language select').val(teamsData.language).attr('selected', 'selected'); + teamEditView.$('.u-field-country select').val(teamsData.country).attr('selected', 'selected'); teamEditView.$('.create-team.form-actions .action-primary').click(); AjaxHelpers.expectJsonRequest(requests, requestMethod(), teamsUrl, teamsData); - AjaxHelpers.respondWithJson(requests, _.extend(_.extend({}, teamsData), teamAction === 'create' ? {id: '123'} : {})); + AjaxHelpers.respondWithJson(requests, _.extend({}, teamsData, teamAction === 'create' ? {id: '123'} : {})); expect(teamEditView.$('.create-team.wrapper-msg .copy').text().trim().length).toBe(0); expect(Backbone.history.navigate.calls[0].args).toContain(expectedUrl); @@ -210,10 +204,10 @@ define([ errorCode, {'user_message': 'User message', 'developer_message': 'Developer message'} ); - expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("User message"); + expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe('User message'); } else { AjaxHelpers.respondWithError(requests); - expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("An error occurred. Please try again."); + expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe('An error occurred. Please try again.'); } }; @@ -234,7 +228,9 @@ define([ }); it('can create a team', function () { - assertTeamCreateUpdateInfo(this, createTeamData, teamsUrl, 'teams/awesomeness/123'); + assertTeamCreateUpdateInfo( + this, createTeamData, teamsUrl, 'teams/' + TeamSpecHelpers.testTopicID + '/123' + ); }); it('shows validation error message when field is empty', function () { @@ -245,16 +241,16 @@ define([ assertValidationMessagesWhenInvalidData(this); }); - it("shows an error message for HTTP 500", function () { + it('shows an error message for HTTP 500', function () { assertShowMessageOnError(this, createTeamData, teamsUrl, 500); }); - it("shows correct error message when server returns an error", function () { + it('shows correct error message when server returns an error', function () { assertShowMessageOnError(this, createTeamData, teamsUrl, 400); }); - it("changes route on cancel click", function () { - assertRedirectsToCorrectUrlOnCancel('topics/awesomeness'); + it('changes route on cancel click', function () { + assertRedirectsToCorrectUrlOnCancel('topics/' + TeamSpecHelpers.testTopicID); }); }); @@ -273,7 +269,10 @@ define([ copyTeamsData.country = 'CA'; copyTeamsData.language = 'fr'; - assertTeamCreateUpdateInfo(this, copyTeamsData, teamsUrl + editTeamID + '?expand=user', 'teams/awesomeness/' + editTeamID); + assertTeamCreateUpdateInfo( + this, copyTeamsData, teamsUrl + editTeamID + '?expand=user', + 'teams/' + TeamSpecHelpers.testTopicID + '/' + editTeamID + ); }); it('shows validation error message when field is empty', function () { @@ -284,16 +283,16 @@ define([ assertValidationMessagesWhenInvalidData(this); }); - it("shows an error message for HTTP 500", function () { + it('shows an error message for HTTP 500', function () { assertShowMessageOnError(this, editTeamData, teamsUrl + editTeamID + '?expand=user', 500); }); - it("shows correct error message when server returns an error", function () { + it('shows correct error message when server returns an error', function () { assertShowMessageOnError(this, editTeamData, teamsUrl + editTeamID + '?expand=user', 400); }); - it("changes route on cancel click", function () { - assertRedirectsToCorrectUrlOnCancel('teams/awesomeness/' + editTeamID); + it('changes route on cancel click', function () { + assertRedirectsToCorrectUrlOnCancel('teams/' + TeamSpecHelpers.testTopicID + '/' + editTeamID); }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js index 92367ef589..beb93e4b80 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/my_teams_spec.js @@ -13,17 +13,16 @@ define([ }); var createMyTeamsView = function(options) { - return new MyTeamsView({ - el: '.teams-container', - collection: options.teams || TeamSpecHelpers.createMockTeams(), - teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(), - showActions: true, - teamParams: { - topicID: 'test-topic', - countries: TeamSpecHelpers.testCountries, - languages: TeamSpecHelpers.testLanguages - } - }).render(); + return new MyTeamsView(_.extend( + { + el: '.teams-container', + collection: options.teams || TeamSpecHelpers.createMockTeams(), + teamMemberships: TeamSpecHelpers.createMockTeamMemberships(), + showActions: true, + context: TeamSpecHelpers.testContext + }, + options + )).render(); }; it('can render itself', function () { @@ -62,15 +61,16 @@ define([ expect(myTeamsView.$el.text().trim()).toBe('You are not currently a member of any team.'); teamMemberships.teamEvents.trigger('teams:update', { action: 'create' }); myTeamsView.render(); - AjaxHelpers.expectJsonRequestURL( + AjaxHelpers.expectRequestURL( requests, - 'api/teams/team_memberships', + TeamSpecHelpers.testContext.teamMembershipsUrl, { - expand : 'team', - username : 'testUser', - course_id : 'my/course/id', + expand : 'team,user', + username : TeamSpecHelpers.testContext.userInfo.username, + course_id : TeamSpecHelpers.testContext.courseID, page : '1', - page_size : '10' + page_size : '10', + text_search: '' } ); AjaxHelpers.respondWithJson(requests, {}); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js index c83da4b963..6de4f1d9f2 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_card_spec.js @@ -3,21 +3,24 @@ define(['jquery', 'teams/js/views/team_card', 'teams/js/models/team'], function ($, _, TeamCardView, Team) { + 'use strict'; + describe('TeamCardView', function () { var createTeamCardView, view; createTeamCardView = function () { var model = new Team({ - id: 'test-team', - name: 'Test Team', - is_active: true, - course_id: 'test/course/id', - topic_id: 'test-topic', - description: 'A team for testing', - last_activity_at: "2015-08-21T18:53:01.145Z", - country: 'us', - language: 'en' - }), - teamCardClass = TeamCardView.extend({ + id: 'test-team', + name: 'Test Team', + is_active: true, + course_id: 'test/course/id', + topic_id: 'test-topic', + description: 'A team for testing', + last_activity_at: "2015-08-21T18:53:01.145Z", + country: 'us', + language: 'en', + membership: [] + }), + TeamCardClass = TeamCardView.extend({ maxTeamSize: '100', srInfo: { id: 'test-sr-id', @@ -26,7 +29,7 @@ define(['jquery', countries: {us: 'United States of America'}, languages: {en: 'English'} }); - return new teamCardClass({ + return new TeamCardClass({ model: model }); }; @@ -50,6 +53,101 @@ define(['jquery', it('navigates to the associated team page when its action button is clicked', function () { expect(view.$('.action').attr('href')).toEqual('#teams/test-topic/test-team'); }); + + describe('Profile Image Thumbnails', function () { + /** + * Takes an array of objects representing team + * members, each having the keys 'username', + * 'image_url', and 'last_activity', and sets the + * teams membership accordingly and re-renders the + * view. + */ + var setMemberships, expectThumbnailsOrder; + + setMemberships = function (memberships) { + view.model.set({ + membership: _.map(memberships, function (m) { + return { + user: {username: m.username, profile_image: {image_url_small: m.image_url}}, + last_activity_at: m.last_activity + }; + }) + }); + view.render(); + }; + + /** + * Takes an array of objects representing team + * members, each having the keys 'username' and + * 'image_url', and expects that the image thumbnails + * rendered on the team card match, in order, the + * members of the provided list. + */ + expectThumbnailsOrder = function (members) { + var thumbnails = view.$('.item-member-thumb img'); + expect(thumbnails.length).toBe(members.length); + thumbnails.each(function (index, imgEl) { + expect(thumbnails.eq(index).attr('alt')).toBe(members[index].username); + expect(thumbnails.eq(index).attr('src')).toBe(members[index].image_url); + }); + }; + + it('displays no thumbnails for an empty team', function () { + view.model.set({membership: []}); + view.render(); + expect(view.$('.item-member-thumb').length).toBe(0); + }); + + it('displays thumbnails for a nonempty team', function () { + var users = [ + { + username: 'user_1', image_url: 'user_1_image', + last_activity: new Date("2010/1/1").toString() + }, { + username: 'user_2', image_url: 'user_2_image', + last_activity: new Date("2011/1/1").toString() + } + ]; + setMemberships(users); + expectThumbnailsOrder([ + {username: 'user_2', image_url: 'user_2_image'}, + {username: 'user_1', image_url: 'user_1_image'}, + ]); + }); + + it('displays thumbnails and an ellipsis for a team with greater than 5 members', function () { + var users = [ + { + username: 'user_1', image_url: 'user_1_image', + last_activity: new Date("2001/1/1").toString() + }, { + username: 'user_2', image_url: 'user_2_image', + last_activity: new Date("2006/1/1").toString() + }, { + username: 'user_3', image_url: 'user_3_image', + last_activity: new Date("2003/1/1").toString() + }, { + username: 'user_4', image_url: 'user_4_image', + last_activity: new Date("2002/1/1").toString() + }, { + username: 'user_5', image_url: 'user_5_image', + last_activity: new Date("2005/1/1").toString() + }, { + username: 'user_6', image_url: 'user_6_image', + last_activity: new Date("2004/1/1").toString() + } + ]; + setMemberships(users); + expectThumbnailsOrder([ + {username: 'user_2', image_url: 'user_2_image'}, + {username: 'user_5', image_url: 'user_5_image'}, + {username: 'user_6', image_url: 'user_6_image'}, + {username: 'user_3', image_url: 'user_3_image'}, + {username: 'user_4', image_url: 'user_4_image'}, + ]); + expect(view.$('.item-member-thumb').eq(-1)).toHaveText('and others…'); + }); + }); }); } ); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js index 5fc597c7f1..af509310ba 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_header_actions_spec.js @@ -10,12 +10,10 @@ define([ createMembershipData, createHeaderActionsView, verifyErrorMessage, - ACCOUNTS_API_URL = '/api/user/v1/accounts/', - TEAMS_URL = '/api/team/v0/teams/', - TEAMS_MEMBERSHIP_URL = '/api/team/v0/team_membership/'; + ACCOUNTS_API_URL = '/api/user/v1/accounts/'; createTeamsUrl = function (teamId) { - return TEAMS_URL + teamId + '?expand=user'; + return TeamSpecHelpers.testContext.teamsUrl + teamId + '?expand=user'; }; createTeamModelData = function (teamId, teamName, membership) { @@ -27,21 +25,22 @@ define([ }; }; - createHeaderActionsView = function(maxTeamSize, currentUsername, teamModelData, showEditButton) { - var teamId = 'teamA'; - - var model = new TeamModel(teamModelData, { parse: true }); + createHeaderActionsView = function(requests, maxTeamSize, currentUsername, teamModelData, showEditButton) { + var model = new TeamModel(teamModelData, { parse: true }), + context = TeamSpecHelpers.createMockContext({ + maxTeamSize: maxTeamSize, + userInfo: TeamSpecHelpers.createMockUserInfo({ + username: currentUsername + }) + }); return new TeamProfileHeaderActionsView( { courseID: TeamSpecHelpers.testCourseID, teamEvents: TeamSpecHelpers.teamEvents, + context: context, model: model, - teamsUrl: createTeamsUrl(teamId), - maxTeamSize: maxTeamSize, - currentUsername: currentUsername, - teamMembershipsUrl: TEAMS_MEMBERSHIP_URL, - topicID: '', + topic: TeamSpecHelpers.createMockTopic(), showEditButton: showEditButton } ).render(); @@ -67,7 +66,7 @@ define([ }); verifyErrorMessage = function (requests, errorMessage, expectedMessage, joinTeam) { - var view = createHeaderActionsView(1, 'ma', createTeamModelData('teamA', 'teamAlpha', [])); + var view = createHeaderActionsView(requests, 1, 'ma', createTeamModelData('teamA', 'teamAlpha', [])); if (joinTeam) { // if we want the error to return when user try to join team, respond with no membership AjaxHelpers.respondWithJson(requests, {"count": 0}); @@ -78,8 +77,9 @@ define([ }; it('can render itself', function () { + var requests = AjaxHelpers.requests(this); var teamModelData = createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma')); - var view = createHeaderActionsView(1, 'ma', teamModelData); + var view = createHeaderActionsView(requests, 1, 'ma', teamModelData); expect(view.$('.join-team').length).toEqual(1); }); @@ -90,14 +90,14 @@ define([ var teamId = 'teamA'; var teamName = 'teamAlpha'; var teamModelData = createTeamModelData(teamId, teamName, []); - var view = createHeaderActionsView(1, currentUsername, teamModelData); + var view = createHeaderActionsView(requests, 1, currentUsername, teamModelData); // a get request will be sent to get user membership info // because current user is not member of current team AjaxHelpers.expectRequest( requests, 'GET', - TEAMS_MEMBERSHIP_URL + '?' + $.param({ + TeamSpecHelpers.testContext.teamMembershipsUrl + '?' + $.param({ 'username': currentUsername, 'course_id': TeamSpecHelpers.testCourseID }) ); @@ -111,7 +111,7 @@ define([ AjaxHelpers.expectRequest( requests, 'POST', - TEAMS_MEMBERSHIP_URL, + TeamSpecHelpers.testContext.teamMembershipsUrl, $.param({'username': currentUsername, 'team_id': teamId}) ); AjaxHelpers.respondWithJson(requests, {}); @@ -135,14 +135,14 @@ define([ it('shows already member message', function () { var requests = AjaxHelpers.requests(this); var currentUsername = 'ma1'; - var view = createHeaderActionsView(1, currentUsername, createTeamModelData('teamA', 'teamAlpha', [])); + var view = createHeaderActionsView(requests, 1, currentUsername, createTeamModelData('teamA', 'teamAlpha', [])); // a get request will be sent to get user membership info // because current user is not member of current team AjaxHelpers.expectRequest( requests, 'GET', - TEAMS_MEMBERSHIP_URL + '?' + $.param({ + TeamSpecHelpers.testContext.teamMembershipsUrl + '?' + $.param({ 'username': currentUsername, 'course_id': TeamSpecHelpers.testCourseID }) ); @@ -156,6 +156,7 @@ define([ it('shows team full message', function () { var requests = AjaxHelpers.requests(this); var view = createHeaderActionsView( + requests, 1, 'ma1', createTeamModelData('teamA', 'teamAlpha', createMembershipData('ma')) @@ -199,7 +200,6 @@ define([ }); it('shows correct error message if initializing the view fails', function () { - // Rendering the view sometimes require fetching user's memberships. This may fail. var requests = AjaxHelpers.requests(this); // verify user_message @@ -225,23 +225,26 @@ define([ view, createAndAssertView; - createAndAssertView = function(showEditButton) { + createAndAssertView = function(requests, showEditButton) { teamModelData = createTeamModelData('aveA', 'avengers', createMembershipData('ma')); - view = createHeaderActionsView(1, 'ma', teamModelData, showEditButton); + view = createHeaderActionsView(requests, 1, 'ma', teamModelData, showEditButton); expect(view.$('.action-edit-team').length).toEqual(showEditButton ? 1 : 0); }; it('renders when option showEditButton is true', function () { - createAndAssertView(true); + var requests = AjaxHelpers.requests(this); + createAndAssertView(requests, true); }); it('does not render when option showEditButton is false', function () { - createAndAssertView(false); + var requests = AjaxHelpers.requests(this); + createAndAssertView(requests, false); }); it("can navigate to correct url", function () { + var requests = AjaxHelpers.requests(this); spyOn(Backbone.history, 'navigate'); - createAndAssertView(true); + createAndAssertView(requests, true); var editButton = view.$('.action-edit-team'); expect(editButton.length).toEqual(1); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js index d2582fcd01..8e295fb611 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/team_profile_spec.js @@ -11,7 +11,7 @@ define([ DEFAULT_MEMBERSHIP = [ { 'user': { - 'username': 'bilbo', + 'username': TeamSpecHelpers.testUser, 'profile_image': { 'has_image': true, 'image_url_medium': '/image-url' @@ -42,20 +42,8 @@ define([ profileView = new TeamProfileView({ teamEvents: TeamSpecHelpers.teamEvents, courseID: TeamSpecHelpers.testCourseID, + context: TeamSpecHelpers.testContext, model: teamModel, - maxTeamSize: options.maxTeamSize || 3, - requestUsername: 'bilbo', - countries : [ - ['', ''], - ['US', 'United States'], - ['CA', 'Canada'] - ], - languages : [ - ['', ''], - ['en', 'English'], - ['fr', 'French'] - ], - teamMembershipDetailUrl: 'api/team/v0/team_membership/team_id,bilbo', setFocusToHeaderFunc: function() { $('.teams-content').focus(); } @@ -88,7 +76,9 @@ define([ $('.prompt.warning .action-primary').click(); // expect a request to DELETE the team membership - AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'api/team/v0/team_membership/test-team,bilbo'); + AjaxHelpers.expectJsonRequest( + requests, 'DELETE', '/api/team/v0/team_membership/test-team,' + TeamSpecHelpers.testUser + ); AjaxHelpers.respondWithNoContent(requests); // expect a request to refetch the user's team memberships @@ -135,7 +125,7 @@ define([ expect(view.$('.team-detail-header').text()).toBe('Team Details'); expect(view.$('.team-country').text()).toContain('United States'); expect(view.$('.team-language').text()).toContain('English'); - expect(view.$('.team-capacity').text()).toContain(members + ' / 3 Members'); + expect(view.$('.team-capacity').text()).toContain(members + ' / 6 Members'); expect(view.$('.team-member').length).toBe(members); expect(Boolean(view.$('.leave-team-link').length)).toBe(memberOfTeam); }; @@ -176,9 +166,9 @@ define([ expect(view.$('.team-user-membership-status').text().trim()).toBe('You are a member of this team.'); // assert tooltip text. - expect(view.$('.member-profile p').text()).toBe('bilbo'); + expect(view.$('.member-profile p').text()).toBe(TeamSpecHelpers.testUser); // assert user profile page url. - expect(view.$('.member-profile').attr('href')).toBe('/u/bilbo'); + expect(view.$('.member-profile').attr('href')).toBe('/u/' + TeamSpecHelpers.testUser); //Verify that the leave team link is present expect(view.$(leaveTeamLinkSelector).text()).toContain('Leave Team'); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js index 7815235301..6809e4adba 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_spec.js @@ -17,11 +17,7 @@ define([ collection: options.teams || TeamSpecHelpers.createMockTeams(), teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(), showActions: true, - teamParams: { - topicID: 'test-topic', - countries: TeamSpecHelpers.testCountries, - languages: TeamSpecHelpers.testLanguages - } + context: TeamSpecHelpers.testContext }).render(); }; diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js index 83fccf20c5..786cb08b63 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/teams_tab_spec.js @@ -8,14 +8,6 @@ define([ 'use strict'; describe('TeamsTab', function () { - var expectContent = function (teamsTabView, text) { - expect(teamsTabView.$('.page-content-main').text()).toContain(text); - }; - - var expectHeader = function (teamsTabView, text) { - expect(teamsTabView.$('.teams-header').text()).toContain(text); - }; - var expectError = function (teamsTabView, text) { expect(teamsTabView.$('.warning').text()).toContain(text); }; @@ -26,30 +18,17 @@ define([ var createTeamsTabView = function(options) { var defaultTopics = { - count: 1, + count: 5, num_pages: 1, current_page: 1, start: 0, - results: [{ - description: 'test description', - name: 'test topic', - id: 'test_topic', - team_count: 0 - }] + results: TeamSpecHelpers.createMockTopicData(1, 5) }, teamsTabView = new TeamsTabView( - _.extend( - { - el: $('.teams-content'), - topics: defaultTopics, - userInfo: TeamSpecHelpers.createMockUserInfo(), - topicsUrl: 'api/topics/', - topicUrl: 'api/topics/topic_id,test/course/id', - teamsUrl: 'api/teams/', - courseID: 'test/course/id' - }, - options || {} - ) + { + el: $('.teams-content'), + context: TeamSpecHelpers.createMockContext(options) + } ); teamsTabView.start(); return teamsTabView; @@ -82,7 +61,7 @@ define([ var requests = AjaxHelpers.requests(this), teamsTabView = createTeamsTabView(); teamsTabView.router.navigate('topics/no_such_topic', {trigger: true}); - AjaxHelpers.expectRequest(requests, 'GET', 'api/topics/no_such_topic,test/course/id', null); + AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/topics/no_such_topic,course/1', null); AjaxHelpers.respondWithError(requests, 404); expectError(teamsTabView, 'The topic "no_such_topic" could not be found.'); expectFocus(teamsTabView.$('.warning')); @@ -91,8 +70,8 @@ define([ it('displays and focuses an error message when trying to navigate to a nonexistent team', function () { var requests = AjaxHelpers.requests(this), teamsTabView = createTeamsTabView(); - teamsTabView.router.navigate('teams/test_topic/no_such_team', {trigger: true}); - AjaxHelpers.expectRequest(requests, 'GET', 'api/teams/no_such_team?expand=user', null); + teamsTabView.router.navigate('teams/' + TeamSpecHelpers.testTopicID + '/no_such_team', {trigger: true}); + AjaxHelpers.expectRequest(requests, 'GET', '/api/team/v0/teams/no_such_team?expand=user', null); AjaxHelpers.respondWithError(requests, 404); expectError(teamsTabView, 'The team "no_such_team" could not be found.'); expectFocus(teamsTabView.$('.warning')); @@ -113,7 +92,7 @@ define([ it('allows access to a team which an unprivileged user is a member of', function () { var teamsTabView = createTeamsTabView({ userInfo: TeamSpecHelpers.createMockUserInfo({ - username: 'test-user', + username: TeamSpecHelpers.testUser, privileged: false }) }); @@ -121,7 +100,7 @@ define([ attributes: { membership: [{ user: { - username: 'test-user' + username: TeamSpecHelpers.testUser } }] } @@ -137,5 +116,103 @@ define([ })).toBe(true); }); }); + + describe('Search', function () { + var verifyTeamsRequest = function(requests, options) { + AjaxHelpers.expectRequestURL(requests, TeamSpecHelpers.testContext.teamsUrl, + _.extend( + { + topic_id: TeamSpecHelpers.testTopicID, + expand: 'user', + course_id: TeamSpecHelpers.testCourseID, + order_by: '', + page: '1', + page_size: '10', + text_search: '' + }, + options + )); + }; + + xit('can search teams', function () { + var requests = AjaxHelpers.requests(this), + teamsTabView = createTeamsTabView(); + teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); + verifyTeamsRequest(requests, { + order_by: 'last_activity_at', + text_search: '' + }); + AjaxHelpers.respondWithJson(requests, {}); + teamsTabView.$('.search-field').val('foo'); + teamsTabView.$('.action-search').click(); + verifyTeamsRequest(requests, { + order_by: '', + text_search: 'foo' + }); + AjaxHelpers.respondWithJson(requests, {}); + expect(teamsTabView.$('.page-title').text()).toBe('Team Search'); + expect(teamsTabView.$('.page-description').text()).toBe('Showing results for "foo"'); + }); + + xit('can clear a search', function () { + var requests = AjaxHelpers.requests(this), + teamsTabView = createTeamsTabView(); + teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); + AjaxHelpers.respondWithJson(requests, {}); + + // Perform a search + teamsTabView.$('.search-field').val('foo'); + teamsTabView.$('.action-search').click(); + AjaxHelpers.respondWithJson(requests, {}); + + // Clear the search and submit it again + teamsTabView.$('.search-field').val(''); + teamsTabView.$('.action-search').click(); + verifyTeamsRequest(requests, { + order_by: 'last_activity_at', + text_search: '' + }); + AjaxHelpers.respondWithJson(requests, {}); + expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1'); + expect(teamsTabView.$('.page-description').text()).toBe('Test description 1'); + }); + + xit('clears the search when navigating away and then back', function () { + var requests = AjaxHelpers.requests(this), + teamsTabView = createTeamsTabView(); + teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); + AjaxHelpers.respondWithJson(requests, {}); + + // Perform a search + teamsTabView.$('.search-field').val('foo'); + teamsTabView.$('.action-search').click(); + AjaxHelpers.respondWithJson(requests, {}); + + // Navigate back to the teams list + teamsTabView.$('.breadcrumbs a').last().click(); + verifyTeamsRequest(requests, { + order_by: 'last_activity_at', + text_search: '' + }); + AjaxHelpers.respondWithJson(requests, {}); + expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1'); + expect(teamsTabView.$('.page-description').text()).toBe('Test description 1'); + }); + + xit('does not switch to showing results when the search returns an error', function () { + var requests = AjaxHelpers.requests(this), + teamsTabView = createTeamsTabView(); + teamsTabView.browseTopic(TeamSpecHelpers.testTopicID); + AjaxHelpers.respondWithJson(requests, {}); + + // Perform a search + teamsTabView.$('.search-field').val('foo'); + teamsTabView.$('.action-search').click(); + AjaxHelpers.respondWithError(requests); + expect(teamsTabView.$('.page-title').text()).toBe('Test Topic 1'); + expect(teamsTabView.$('.page-description').text()).toBe('Test description 1'); + expect(teamsTabView.$('.search-field').val(), 'foo'); + }); + }); }); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js index 415ada244c..902fb01208 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/topic_teams_spec.js @@ -11,14 +11,11 @@ define([ var createTopicTeamsView = function(options) { return new TopicTeamsView({ el: '.teams-container', + model: TeamSpecHelpers.createMockTopic(), collection: options.teams || TeamSpecHelpers.createMockTeams(), teamMemberships: options.teamMemberships || TeamSpecHelpers.createMockTeamMemberships(), showActions: true, - teamParams: { - topicID: 'test-topic', - countries: TeamSpecHelpers.testCountries, - languages: TeamSpecHelpers.testLanguages - } + context: TeamSpecHelpers.testContext }).render(); }; @@ -27,8 +24,8 @@ define([ options = {showActions: true}; } var expectedTitle = 'Are you having trouble finding a team to join?', - expectedMessage = 'Try browsing all teams or searching team descriptions. If you ' + - 'still can\'t find a team to join, create a new team in this topic.', + expectedMessage = 'Browse teams in other topics or search teams in this topic. ' + + 'If you still can\'t find a team to join, create a new team in this topic.', title = teamsView.$('.title').text().trim(), message = teamsView.$('.copy').text().trim(); if (options.showActions) { @@ -65,17 +62,16 @@ define([ var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]), teamsView = createTopicTeamsView({ teamMemberships: emptyMembership }); spyOn(Backbone.history, 'navigate'); - teamsView.$('a.browse-teams').click(); + teamsView.$('.browse-teams').click(); expect(Backbone.history.navigate.calls[0].args).toContain('browse'); }); - it('can search teams', function () { + xit('gives the search field focus when clicking on the search teams link', function () { var emptyMembership = TeamSpecHelpers.createMockTeamMemberships([]), teamsView = createTopicTeamsView({ teamMemberships: emptyMembership }); - spyOn(Backbone.history, 'navigate'); - teamsView.$('a.search-teams').click(); - // TODO! Should be updated once team description search feature is available - expect(Backbone.history.navigate.calls[0].args).toContain('browse'); + spyOn($.fn, 'focus').andCallThrough(); + teamsView.$('.search-teams').click(); + expect(teamsView.$('.search-field').first().focus).toHaveBeenCalled(); }); it('can show the create team modal', function () { @@ -83,7 +79,9 @@ define([ teamsView = createTopicTeamsView({ teamMemberships: emptyMembership }); spyOn(Backbone.history, 'navigate'); teamsView.$('a.create-team').click(); - expect(Backbone.history.navigate.calls[0].args).toContain('topics/test-topic/create-team'); + expect(Backbone.history.navigate.calls[0].args).toContain( + 'topics/' + TeamSpecHelpers.testTopicID + '/create-team' + ); }); it('does not show actions for a user already in a team', function () { @@ -118,13 +116,13 @@ define([ verifyActions(teamsView, {showActions: true}); teamMemberships.teamEvents.trigger('teams:update', { action: 'create' }); teamsView.render(); - AjaxHelpers.expectJsonRequestURL( + AjaxHelpers.expectRequestURL( requests, 'foo', { expand : 'team', username : 'testUser', - course_id : 'my/course/id', + course_id : TeamSpecHelpers.testCourseID, page : '1', page_size : '10' } diff --git a/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js b/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js index abf39839c4..049339675f 100644 --- a/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js +++ b/lms/djangoapps/teams/static/teams/js/spec/views/topics_spec.js @@ -10,7 +10,8 @@ define([ return new TopicsView({ teamEvents: TeamSpecHelpers.teamEvents, el: '.topics-container', - collection: topicCollection + collection: topicCollection, + context: TeamSpecHelpers.createMockContext() }).render(); }; @@ -48,14 +49,15 @@ define([ topicsView = createTopicsView(); triggerUpdateEvent(topicsView); - AjaxHelpers.expectJsonRequestURL( + AjaxHelpers.expectRequestURL( requests, - 'api/teams/topics', + TeamSpecHelpers.testContext.topicUrl, { - course_id : 'my/course/id', - page : '1', - page_size : '5', // currently the page size is determined by the size of the collection - order_by : 'name' + course_id: TeamSpecHelpers.testCourseID, + page: '1', + page_size: '5', // currently the page size is determined by the size of the collection + order_by: 'name', + text_search: '' } ); }); @@ -66,14 +68,15 @@ define([ // Staff are not immediately added to the team, but may choose to join after the create event. triggerUpdateEvent(topicsView, true); - AjaxHelpers.expectJsonRequestURL( + AjaxHelpers.expectRequestURL( requests, - 'api/teams/topics', + TeamSpecHelpers.testContext.topicUrl, { - course_id : 'my/course/id', - page : '1', - page_size : '5', // currently the page size is determined by the size of the collection - order_by : 'name' + course_id: TeamSpecHelpers.testCourseID, + page: '1', + page_size: '5', // currently the page size is determined by the size of the collection + order_by: 'name', + text_search: '' } ); }); diff --git a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js index a210bf7c4e..2c51ef5ae2 100644 --- a/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js +++ b/lms/djangoapps/teams/static/teams/js/spec_helpers/team_spec_helpers.js @@ -3,13 +3,15 @@ define([ 'underscore', 'teams/js/collections/team', 'teams/js/collections/team_membership', - 'teams/js/collections/topic' -], function (Backbone, _, TeamCollection, TeamMembershipCollection, TopicCollection) { + 'teams/js/collections/topic', + 'teams/js/models/topic' +], function (Backbone, _, TeamCollection, TeamMembershipCollection, TopicCollection, TopicModel) { 'use strict'; var createMockPostResponse, createMockDiscussionResponse, createAnnotatedContentInfo, createMockThreadResponse, - createMockTopicData, createMockTopicCollection, + createMockTopicData, createMockTopicCollection, createMockTopic, testCourseID = 'course/1', testUser = 'testUser', + testTopicID = 'test-topic-1', testTeamDiscussionID = "12345", teamEvents = _.clone(Backbone.Events), testCountries = [ @@ -32,7 +34,6 @@ define([ id: "id " + i, language: testLanguages[i%4][0], country: testCountries[i%4][0], - is_active: true, membership: [], last_activity_at: '' }; @@ -53,7 +54,7 @@ define([ }, { teamEvents: teamEvents, - course_id: 'my/course/id', + course_id: testCourseID, parse: true } ); @@ -82,18 +83,22 @@ define([ num_pages: 3, current_page: 1, start: 0, + sort_order: 'last_activity_at', results: teamMembershipData }, - _.extend(_.extend({}, { + _.extend( + {}, + { teamEvents: teamEvents, - course_id: 'my/course/id', + course_id: testCourseID, parse: true, - url: 'api/teams/team_memberships', + url: testContext.teamMembershipsUrl, username: testUser, privileged: false, staff: false - }), - options) + }, + options + ) ); }; @@ -145,7 +150,7 @@ define([ group_id: 1, endorsed: false }, - options || {} + options ); }; @@ -229,21 +234,56 @@ define([ context: "standalone", endorsed: false }, - options || {} + options ); }; createMockTopicData = function (startIndex, stopIndex) { return _.map(_.range(startIndex, stopIndex + 1), function (i) { return { - "description": "description " + i, - "name": "topic " + i, - "id": "id " + i, + "description": "Test description " + i, + "name": "Test Topic " + i, + "id": "test-topic-" + i, "team_count": 0 }; }); }; + createMockTopic = function(options) { + return new TopicModel(_.extend( + { + id: testTopicID, + name: 'Test Topic 1', + description: 'Test description 1' + }, + options + )); + }; + + var testContext = { + courseID: testCourseID, + topics: { + count: 5, + num_pages: 1, + current_page: 1, + start: 0, + results: createMockTopicData(1, 5) + }, + maxTeamSize: 6, + languages: testLanguages, + countries: testCountries, + topicUrl: '/api/team/v0/topics/topic_id,' + testCourseID, + teamsUrl: '/api/team/v0/teams/', + teamsDetailUrl: '/api/team/v0/teams/team_id', + teamMembershipsUrl: '/api/team/v0/team_memberships/', + teamMembershipDetailUrl: '/api/team/v0/team_membership/team_id,' + testUser, + userInfo: createMockUserInfo() + }; + + var createMockContext = function(options) { + return _.extend({}, testContext, options); + }; + createMockTopicCollection = function (topicData) { topicData = topicData !== undefined ? topicData : createMockTopicData(1, 5); @@ -254,13 +294,13 @@ define([ num_pages: 2, start: 0, results: topicData, - sort_order: "name" + sort_order: 'name' }, { teamEvents: teamEvents, - course_id: 'my/course/id', + course_id: testCourseID, parse: true, - url: 'api/teams/topics' + url: testContext.topicUrl } ); }; @@ -269,14 +309,18 @@ define([ teamEvents: teamEvents, testCourseID: testCourseID, testUser: testUser, + testTopicID: testTopicID, testCountries: testCountries, testLanguages: testLanguages, testTeamDiscussionID: testTeamDiscussionID, + testContext: testContext, createMockTeamData: createMockTeamData, createMockTeams: createMockTeams, createMockTeamMembershipsData: createMockTeamMembershipsData, createMockTeamMemberships: createMockTeamMemberships, createMockUserInfo: createMockUserInfo, + createMockContext: createMockContext, + createMockTopic: createMockTopic, createMockPostResponse: createMockPostResponse, createMockDiscussionResponse: createMockDiscussionResponse, createAnnotatedContentInfo: createAnnotatedContentInfo, diff --git a/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js index 441e1e11f6..d736d4da99 100644 --- a/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js +++ b/lms/djangoapps/teams/static/teams/js/teams_tab_factory.js @@ -4,7 +4,10 @@ define(['jquery', 'underscore', 'backbone', 'teams/js/views/teams_tab'], function ($, _, Backbone, TeamsTabView) { return function (options) { - var teamsTab = new TeamsTabView(_.extend(options, {el: $('.teams-content')})); + var teamsTab = new TeamsTabView({ + el: $('.teams-content'), + context: options + }); teamsTab.start(); }; }); diff --git a/lms/djangoapps/teams/static/teams/js/views/edit_team.js b/lms/djangoapps/teams/static/teams/js/views/edit_team.js index 5f8b3f2462..fc6ca4616e 100644 --- a/lms/djangoapps/teams/static/teams/js/views/edit_team.js +++ b/lms/djangoapps/teams/static/teams/js/views/edit_team.js @@ -21,25 +21,20 @@ initialize: function(options) { this.teamEvents = options.teamEvents; - this.courseID = options.teamParams.courseID; - this.topicID = options.teamParams.topicID; + this.context = options.context; + this.topic = options.topic; this.collection = options.collection; - this.teamsUrl = options.teamParams.teamsUrl; - this.languages = options.teamParams.languages; - this.countries = options.teamParams.countries; - this.teamsDetailUrl = options.teamParams.teamsDetailUrl; this.action = options.action; - _.bindAll(this, 'cancelAndGoBack', 'createOrUpdateTeam'); - if (this.action === 'create') { this.teamModel = new TeamModel({}); - this.teamModel.url = this.teamsUrl; - this.primaryButtonTitle = 'Create'; + this.teamModel.url = this.context.teamsUrl; + this.primaryButtonTitle = gettext("Create"); } else if(this.action === 'edit' ) { this.teamModel = options.model; - this.teamModel.url = this.teamsDetailUrl.replace('team_id', options.model.get('id')) + '?expand=user'; - this.primaryButtonTitle = 'Update'; + this.teamModel.url = this.context.teamsDetailUrl.replace('team_id', options.model.get('id')) + + '?expand=user'; + this.primaryButtonTitle = gettext("Update"); } this.teamNameField = new FieldViews.TextFieldView({ @@ -65,7 +60,7 @@ required: false, showMessages: false, titleIconName: 'fa-comment-o', - options: this.languages, + options: this.context.languages, helpMessage: gettext('The language that team members primarily use to communicate with each other.') }); @@ -76,7 +71,7 @@ required: false, showMessages: false, titleIconName: 'fa-globe', - options: this.countries, + options: this.context.countries, helpMessage: gettext('The country that team members primarily identify with.') }); }, @@ -119,8 +114,8 @@ }; if (this.action === 'create') { - data.course_id = this.courseID; - data.topic_id = this.topicID; + data.course_id = this.context.courseID; + data.topic_id = this.topic.id; } else if (this.action === 'edit' ) { saveOptions.patch = true; saveOptions.contentType = 'application/merge-patch+json'; @@ -139,7 +134,7 @@ team: result }); Backbone.history.navigate( - 'teams/' + view.topicID + '/' + view.teamModel.id, + 'teams/' + view.topic.id + '/' + view.teamModel.id, {trigger: true} ); }) @@ -210,9 +205,9 @@ event.preventDefault(); var url; if (this.action === 'create') { - url = 'topics/' + this.topicID; + url = 'topics/' + this.topic.id; } else if (this.action === 'edit' ) { - url = 'teams/' + this.topicID + '/' + this.teamModel.get('id'); + url = 'teams/' + this.topic.id + '/' + this.teamModel.get('id'); } Backbone.history.navigate(url, {trigger: true}); } diff --git a/lms/djangoapps/teams/static/teams/js/views/team_card.js b/lms/djangoapps/teams/static/teams/js/views/team_card.js index afaf8200e8..2151018d85 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_card.js @@ -7,34 +7,43 @@ 'jquery.timeago', 'js/components/card/views/card', 'teams/js/views/team_utils', + 'text!teams/templates/team-membership-details.underscore', 'text!teams/templates/team-country-language.underscore', 'text!teams/templates/team-activity.underscore' - ], function (Backbone, _, gettext, timeago, CardView, TeamUtils, teamCountryLanguageTemplate, teamActivityTemplate) { + ], function ( + Backbone, + _, + gettext, + timeago, + CardView, + TeamUtils, + teamMembershipDetailsTemplate, + teamCountryLanguageTemplate, + teamActivityTemplate + ) { var TeamMembershipView, TeamCountryLanguageView, TeamActivityView, TeamCardView; TeamMembershipView = Backbone.View.extend({ tagName: 'div', className: 'team-members', - template: _.template( - '<span class="member-count"><%= membership_message %></span>' + - '<ul class="list-member-thumbs"></ul>' - ), + template: _.template(teamMembershipDetailsTemplate), initialize: function (options) { this.maxTeamSize = options.maxTeamSize; }, render: function () { - var memberships = this.model.get('membership'), + var allMemberships = _(this.model.get('membership')) + .sortBy(function (member) {return new Date(member.last_activity_at);}).reverse(), + displayableMemberships = allMemberships.slice(0, 5), maxMemberCount = this.maxTeamSize; this.$el.html(this.template({ - membership_message: TeamUtils.teamCapacityText(memberships.length, maxMemberCount) + membership_message: TeamUtils.teamCapacityText(allMemberships.length, maxMemberCount), + memberships: displayableMemberships, + has_additional_memberships: displayableMemberships.length < allMemberships.length, + // Translators: "and others" refers to fact that additional members of a team exist that are not displayed. + sr_message: gettext('and others') })); - _.each(memberships, function (membership) { - this.$('list-member-thumbs').append( - '<li class="item-member-thumb"><img alt="' + membership.user.username + '" src=""></img></li>' - ); - }, this); return this; } }); diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index 10beac0a7e..0f49d7c934 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -18,15 +18,11 @@ }, initialize: function (options) { this.teamEvents = options.teamEvents; - this.courseID = options.courseID; - this.maxTeamSize = options.maxTeamSize; - this.requestUsername = options.requestUsername; - this.isPrivileged = options.isPrivileged; - this.teamMembershipDetailUrl = options.teamMembershipDetailUrl; + this.context = options.context; this.setFocusToHeaderFunc = options.setFocusToHeaderFunc; - this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(options.countries); - this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(options.languages); + this.countries = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries); + this.languages = TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages); this.listenTo(this.model, "change", this.render); }, @@ -34,18 +30,17 @@ render: function () { var memberships = this.model.get('membership'), discussionTopicID = this.model.get('discussion_topic_id'), - isMember = TeamUtils.isUserMemberOfTeam(memberships, this.requestUsername); + isMember = TeamUtils.isUserMemberOfTeam(memberships, this.context.userInfo.username); this.$el.html(_.template(teamTemplate, { - courseID: this.courseID, + courseID: this.context.courseID, discussionTopicID: discussionTopicID, - readOnly: !(this.isPrivileged || isMember), + readOnly: !(this.context.userInfo.privileged || isMember), country: this.countries[this.model.get('country')], language: this.languages[this.model.get('language')], - membershipText: TeamUtils.teamCapacityText(memberships.length, this.maxTeamSize), + membershipText: TeamUtils.teamCapacityText(memberships.length, this.context.maxTeamSize), isMember: isMember, - hasCapacity: memberships.length < this.maxTeamSize, + hasCapacity: memberships.length < this.context.maxTeamSize, hasMembers: memberships.length >= 1 - })); this.discussionView = new TeamDiscussionView({ el: this.$('.discussion-module') @@ -84,7 +79,7 @@ function() { $.ajax({ type: 'DELETE', - url: view.teamMembershipDetailUrl.replace('team_id', view.model.get('id')) + url: view.context.teamMembershipDetailUrl.replace('team_id', view.model.get('id')) }).done(function (data) { view.model.fetch() .done(function() { diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js index a6b6390034..e05783ca71 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js @@ -21,22 +21,19 @@ initialize: function(options) { this.teamEvents = options.teamEvents; this.template = _.template(teamProfileHeaderActionsTemplate); - this.courseID = options.courseID; - this.maxTeamSize = options.maxTeamSize; - this.currentUsername = options.currentUsername; - this.teamMembershipsUrl = options.teamMembershipsUrl; + this.context = options.context; this.showEditButton = options.showEditButton; - this.topicID = options.topicID; - _.bindAll(this, 'render', 'joinTeam','editTeam', 'getUserTeamInfo'); + this.topic = options.topic; this.listenTo(this.model, "change", this.render); }, render: function() { var view = this, + username = this.context.userInfo.username, message, showJoinButton, teamHasSpace; - this.getUserTeamInfo(this.currentUsername, view.maxTeamSize).done(function (info) { + this.getUserTeamInfo(username, this.context.maxTeamSize).done(function (info) { teamHasSpace = info.teamHasSpace; // if user is the member of current team then we wouldn't show anything @@ -63,8 +60,8 @@ var view = this; $.ajax({ type: 'POST', - url: view.teamMembershipsUrl, - data: {'username': view.currentUsername, 'team_id': view.model.get('id')} + url: view.context.teamMembershipsUrl, + data: {'username': view.context.userInfo.username, 'team_id': view.model.get('id')} }).done(function (data) { view.model.fetch() .done(function() { @@ -98,8 +95,8 @@ var view = this; $.ajax({ type: 'GET', - url: view.teamMembershipsUrl, - data: {'username': username, 'course_id': view.courseID} + url: view.context.teamMembershipsUrl, + data: {'username': username, 'course_id': view.context.courseID} }).done(function (data) { info.alreadyMember = (data.count > 0); info.memberOfCurrentTeam = false; @@ -116,9 +113,13 @@ return deferred.promise(); }, + editTeam: function (event) { event.preventDefault(); - Backbone.history.navigate('topics/' + this.topicID + '/' + this.model.get('id') +'/edit-team', {trigger: true}); + Backbone.history.navigate( + 'topics/' + this.topic.id + '/' + this.model.get('id') +'/edit-team', + {trigger: true} + ); } }); }); diff --git a/lms/djangoapps/teams/static/teams/js/views/teams.js b/lms/djangoapps/teams/static/teams/js/views/teams.js index f25adaa2df..be8d8b14f6 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams.js @@ -10,10 +10,6 @@ var TeamsView = PaginatedView.extend({ type: 'teams', - events: { - 'click button.action': '' // entry point for team creation - }, - srInfo: { id: "heading-browse-teams", text: gettext('All teams') @@ -22,14 +18,14 @@ initialize: function (options) { this.topic = options.topic; this.teamMemberships = options.teamMemberships; - this.teamParams = options.teamParams; + this.context = options.context; this.itemViewClass = TeamCardView.extend({ router: options.router, topic: options.topic, - maxTeamSize: options.maxTeamSize, + maxTeamSize: this.context.maxTeamSize, srInfo: this.srInfo, - countries: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.countries), - languages: TeamUtils.selectorOptionsArrayToHashWithBlank(options.teamParams.languages) + countries: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.countries), + languages: TeamUtils.selectorOptionsArrayToHashWithBlank(this.context.languages) }); PaginatedView.prototype.initialize.call(this); } diff --git a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js index 461159ecc3..ae36b4fd09 100644 --- a/lms/djangoapps/teams/static/teams/js/views/teams_tab.js +++ b/lms/djangoapps/teams/static/teams/js/views/teams_tab.js @@ -4,6 +4,7 @@ define(['backbone', 'underscore', 'gettext', + 'common/js/components/views/search_field', 'js/components/header/views/header', 'js/components/header/models/header', 'js/components/tabbed/views/tabbed_view', @@ -19,12 +20,12 @@ 'teams/js/views/edit_team', 'teams/js/views/team_profile_header_actions', 'text!teams/templates/teams_tab.underscore'], - function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView, + function (Backbone, _, gettext, SearchFieldView, HeaderView, HeaderModel, TabbedView, TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection, TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView, TeamProfileHeaderActionsView, teamsTemplate) { var TeamsHeaderModel = HeaderModel.extend({ - initialize: function (attributes) { + initialize: function () { _.extend(this.defaults, {nav_aria_label: gettext('teams')}); HeaderModel.prototype.initialize.call(this); } @@ -48,18 +49,7 @@ var TeamTabView = Backbone.View.extend({ initialize: function(options) { var router; - this.courseID = options.courseID; - this.topics = options.topics; - this.topicUrl = options.topicUrl; - this.teamsUrl = options.teamsUrl; - this.teamsDetailUrl = options.teamsDetailUrl; - this.teamMembershipsUrl = options.teamMembershipsUrl; - this.teamMembershipDetailUrl = options.teamMembershipDetailUrl; - this.maxTeamSize = options.maxTeamSize; - this.languages = options.languages; - this.countries = options.countries; - this.userInfo = options.userInfo; - this.teamsBaseUrl = options.teamsBaseUrl; + this.context = options.context; // This slightly tedious approach is necessary // to use regular expressions within Backbone // routes, allowing us to capture which tab @@ -74,6 +64,7 @@ // being picked up by the backbone router. }, this)], ['topics/:topic_id(/)', _.bind(this.browseTopic, this)], + ['topics/:topic_id/search(/)', _.bind(this.searchTeams, this)], ['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)], ['topics/:topic_id/:team_id/edit-team(/)', _.bind(this.editTeam, this)], ['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)], @@ -87,14 +78,14 @@ this.teamEvents = _.clone(Backbone.Events); this.teamMemberships = new TeamMembershipCollection( - this.userInfo.team_memberships_data, + this.context.userInfo.team_memberships_data, { teamEvents: this.teamEvents, - url: this.teamMembershipsUrl, - course_id: this.courseID, - username: this.userInfo.username, - privileged: this.userInfo.privileged, - staff: this.userInfo.staff, + url: this.context.teamMembershipsUrl, + course_id: this.context.courseID, + username: this.context.userInfo.username, + privileged: this.context.userInfo.privileged, + staff: this.context.userInfo.staff, parse: true } ).bootstrap(); @@ -102,23 +93,17 @@ this.myTeamsView = new MyTeamsView({ router: this.router, teamEvents: this.teamEvents, + context: this.context, collection: this.teamMemberships, - teamMemberships: this.teamMemberships, - maxTeamSize: this.maxTeamSize, - teamParams: { - courseID: this.courseID, - teamsUrl: this.teamsUrl, - languages: this.languages, - countries: this.countries - } + teamMemberships: this.teamMemberships }); this.topicsCollection = new TopicCollection( - this.topics, + this.context.topics, { teamEvents: this.teamEvents, - url: options.topicsUrl, - course_id: this.courseID, + url: this.context.topicsUrl, + course_id: this.context.courseID, parse: true } ).bootstrap(); @@ -129,21 +114,19 @@ collection: this.topicsCollection }); - this.mainView = this.tabbedView = new ViewWithHeader({ - header: new HeaderView({ - model: new TeamsHeaderModel({ - description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."), - title: gettext("Teams") - }) - }), - main: new TabbedView({ + this.mainView = this.tabbedView = this.createViewWithHeader({ + title: gettext("Teams"), + description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."), + mainView: new TabbedView({ tabs: [{ title: gettext('My Team'), url: 'my-teams', view: this.myTeamsView }, { title: interpolate( - // Translators: sr_start and sr_end surround text meant only for screen readers. The whole string will be shown to users as "Browse teams" if they are using a screenreader, and "Browse" otherwise. + // Translators: sr_start and sr_end surround text meant only for screen readers. + // The whole string will be shown to users as "Browse teams" if they are using a + // screenreader, and "Browse" otherwise. gettext("Browse %(sr_start)s teams %(sr_end)s"), {"sr_start": '<span class="sr">', "sr_end": '</span>'}, true ), @@ -190,36 +173,50 @@ }); }, + /** + * Show the search results for a team. + */ + searchTeams: function (topicID) { + var view = this; + if (!this.teamsCollection) { + this.router.navigate('topics/' + topicID, {trigger: true}); + } else { + this.getTopic(topicID).done(function (topic) { + view.mainView = view.createTeamsListView({ + topic: topic, + collection: view.teamsCollection, + title: gettext('Team Search'), + description: interpolate( + gettext('Showing results for "%(searchString)s"'), + { searchString: view.teamsCollection.searchString }, + true + ), + breadcrumbs: view.createBreadcrumbs(topic), + showSortControls: false + }); + view.render(); + }); + } + }, + /** * Render the create new team form. */ newTeam: function (topicID) { - var self = this, - createViewWithHeader; + var view = this; this.getTopic(topicID).done(function (topic) { - var view = new TeamEditView({ - action: 'create', - teamEvents: self.teamEvents, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - } + view.mainView = view.createViewWithHeader({ + topic: topic, + title: gettext("Create a New Team"), + description: gettext("Create a new team if you can't find an existing team to join, or if you would like to learn with friends you know."), + mainView: new TeamEditView({ + action: 'create', + teamEvents: view.teamEvents, + context: view.context, + topic: topic + }) }); - createViewWithHeader = self.createViewWithHeader({ - mainView: view, - subject: { - name: gettext("Create a New Team"), - description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know.") - }, - parentTopic: topic - }); - self.mainView = createViewWithHeader; - self.render(); + view.render(); }); }, @@ -234,27 +231,17 @@ var view = new TeamEditView({ action: 'edit', teamEvents: self.teamEvents, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - }, + context: self.context, + topic: topic, model: team }); editViewWithHeader = self.createViewWithHeader({ - mainView: view, - subject: { - name: gettext("Edit Team"), - description: gettext("If you make significant changes, make sure you notify members of the team before making these changes.") - }, - parentTeam: team, - parentTopic: topic - } - ); + title: gettext("Edit Team"), + description: gettext("If you make significant changes, make sure you notify members of the team before making these changes."), + mainView: view, + topic: topic, + team: team + }); self.mainView = editViewWithHeader; self.render(); }); @@ -267,54 +254,74 @@ getTeamsView: function (topicID) { // Lazily load the teams-for-topic view in // order to avoid making an extra AJAX call. - var self = this, - router = this.router, + var view = this, deferred = $.Deferred(); - if (this.teamsCollection && this.teamsCollection.topic_id === topicID && this.teamsView) { + if (this.teamsView && this.teamsCollection && this.teamsCollection.topic_id === topicID) { + this.teamsCollection.setSearchString(''); deferred.resolve(this.teamsView); } else { this.getTopic(topicID) .done(function(topic) { var collection = new TeamCollection([], { - teamEvents: self.teamEvents, - course_id: self.courseID, + teamEvents: view.teamEvents, + course_id: view.context.courseID, topic_id: topicID, - url: self.teamsUrl, + url: view.context.teamsUrl, per_page: 10 }); - self.teamsCollection = collection; + view.teamsCollection = collection; collection.goTo(1) .done(function() { - var teamsView = new TopicTeamsView({ - router: self.router, + var teamsView = view.createTeamsListView({ topic: topic, collection: collection, - teamMemberships: self.teamMemberships, - maxTeamSize: self.maxTeamSize, - teamParams: { - courseID: self.courseID, - topicID: topic.get('id'), - teamsUrl: self.teamsUrl, - topicName: topic.get('name'), - languages: self.languages, - countries: self.countries, - teamsDetailUrl: self.teamsDetailUrl - } + showSortControls: true }); - deferred.resolve( - self.createViewWithHeader( - { - mainView: teamsView, - subject: topic - } - ) - ); + deferred.resolve(teamsView); }); }); } return deferred.promise(); }, + createTeamsListView: function(options) { + var topic = options.topic, + collection = options.collection, + teamsView = new TopicTeamsView({ + router: this.router, + context: this.context, + model: topic, + collection: collection, + teamMemberships: this.teamMemberships, + showSortControls: options.showSortControls + }), + searchFieldView = new SearchFieldView({ + type: 'teams', + label: gettext('Search teams'), + collection: collection + }), + viewWithHeader = this.createViewWithHeader({ + subject: topic, + mainView: teamsView, + headerActionsView: null, // TODO: add back SearchFieldView when search is enabled + title: options.title, + description: options.description, + breadcrumbs: options.breadcrumbs + }); + // Listen to requests to sync the collection and redirect it as follows: + // 1. If the collection includes a search, show the search results page + // 2. If not, then show the regular topic teams page + // Note: Backbone makes this a no-op if redirecting to the current page. + this.listenTo(collection, 'sync', function() { + if (collection.searchString) { + Backbone.history.navigate('topics/' + topic.get('id') + '/search', {trigger: true}); + } else { + Backbone.history.navigate('topics/' + topic.get('id'), {trigger: true}); + } + }); + return viewWithHeader; + }, + /** * Browse to the team with the specified team ID belonging to the specified topic. */ @@ -338,41 +345,30 @@ */ getBrowseTeamView: function (topicID, teamID) { var self = this, - deferred = $.Deferred(), - courseID = this.courseID; + deferred = $.Deferred(); self.getTopic(topicID).done(function(topic) { self.getTeam(teamID, true).done(function(team) { var view = new TeamProfileView({ teamEvents: self.teamEvents, router: self.router, - courseID: courseID, + context: self.context, model: team, - maxTeamSize: self.maxTeamSize, - isPrivileged: self.userInfo.privileged, - requestUsername: self.userInfo.username, - countries: self.countries, - languages: self.languages, - teamMembershipDetailUrl: self.teamMembershipDetailUrl, setFocusToHeaderFunc: self.setFocusToHeader }); var TeamProfileActionsView = new TeamProfileHeaderActionsView({ teamEvents: self.teamEvents, - courseID: courseID, + context: self.context, model: team, - teamsUrl: self.teamsUrl, - maxTeamSize: self.maxTeamSize, - currentUsername: self.userInfo.username, - teamMembershipsUrl: self.teamMembershipsUrl, - topicID: topicID, - showEditButton: self.userInfo.privileged || self.userInfo.staff + topic: topic, + showEditButton: self.context.userInfo.privileged || self.context.userInfo.staff }); deferred.resolve( self.createViewWithHeader( { mainView: view, subject: team, - parentTopic: topic, + topic: topic, headerActionsView: TeamProfileActionsView } ) @@ -382,52 +378,55 @@ return deferred.promise(); }, - createViewWithHeader: function (options) { - var router = this.router, - breadcrumbs, headerView, - viewDescription, viewTitle; - breadcrumbs = [{ + createBreadcrumbs: function(topic, team) { + var breadcrumbs = [{ title: gettext('All Topics'), url: '#browse' }]; - if (options.parentTopic) { + if (topic) { breadcrumbs.push({ - title: options.parentTopic.get('name'), - url: '#topics/' + options.parentTopic.id + title: topic.get('name'), + url: '#topics/' + topic.id }); - } - if (options.parentTeam) { - breadcrumbs.push({ - title: options.parentTeam.get('name'), - url: '#teams/' + options.parentTopic.id + '/' + options.parentTeam.id - }); - } - if (options.subject instanceof Backbone.Model) { - viewDescription = options.subject.get('description'); - viewTitle = options.subject.get('name'); - - } else if (options.subject) { - viewDescription = options.subject.description; - viewTitle = options.subject.name; - } - - headerView = new HeaderView({ - model: new TeamsHeaderModel({ - description: viewDescription, - title: viewTitle, - breadcrumbs: breadcrumbs - }), - headerActionsView: options.headerActionsView, - events: { - 'click nav.breadcrumbs a.nav-item': function (event) { - var url = $(event.currentTarget).attr('href'); - event.preventDefault(); - router.navigate(url, {trigger: true}); - } + if (team) { + breadcrumbs.push({ + title: team.get('name'), + url: '#teams/' + topic.id + '/' + team.id + }); } + } + return breadcrumbs; + }, + + createHeaderModel: function(options) { + var subject = options.subject, + breadcrumbs = options.breadcrumbs, + title = options.title || subject.get('name'), + description = options.description || subject.get('description'); + if (!breadcrumbs) { + breadcrumbs = this.createBreadcrumbs(options.topic, options.team); + } + return new TeamsHeaderModel({ + breadcrumbs: breadcrumbs, + title: title, + description: description }); + }, + + createViewWithHeader: function(options) { + var router = this.router; return new ViewWithHeader({ - header: headerView, + header: new HeaderView({ + model: this.createHeaderModel(options), + headerActionsView: options.headerActionsView, + events: { + 'click nav.breadcrumbs a.nav-item': function (event) { + var url = $(event.currentTarget).attr('href'); + event.preventDefault(); + router.navigate(url, {trigger: true}); + } + } + }), main: options.mainView }); }, @@ -450,7 +449,7 @@ } else { topic = new TopicModel({ id: topicID, - url: self.topicUrl.replace('topic_id', topicID) + url: self.context.topicUrl.replace('topic_id', topicID) }); topic.fetch() .done(function() { @@ -476,7 +475,7 @@ var team = this.teamsCollection ? this.teamsCollection.get(teamID) : null, self = this, deferred = $.Deferred(), - teamUrl = this.teamsUrl + teamID + (expandUser ? '?expand=user': ''); + teamUrl = this.context.teamsUrl + teamID + (expandUser ? '?expand=user': ''); if (team) { team.url = teamUrl; deferred.resolve(team); @@ -570,11 +569,11 @@ * belongs to the team. */ readOnlyDiscussion: function (team) { - var self = this; + var userInfo = this.context.userInfo; return !( - self.userInfo.privileged || + userInfo.privileged || _.any(team.attributes.membership, function (membership) { - return membership.user.username === self.userInfo.username; + return membership.user.username === userInfo.username; }) ); } diff --git a/lms/djangoapps/teams/static/teams/js/views/topic_card.js b/lms/djangoapps/teams/static/teams/js/views/topic_card.js index c64ebcd619..b1d5292dde 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topic_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/topic_card.js @@ -36,6 +36,7 @@ configuration: 'square_card', cardClass: 'topic-card', + pennant: gettext('Topic'), title: function () { return this.model.get('name'); }, description: function () { return this.model.get('description'); }, details: function () { return this.detailViews; }, diff --git a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js index da8b670fc7..2d23304bd4 100644 --- a/lms/djangoapps/teams/static/teams/js/views/topic_teams.js +++ b/lms/djangoapps/teams/static/teams/js/views/topic_teams.js @@ -1,9 +1,12 @@ ;(function (define) { 'use strict'; - - define(['backbone', 'gettext', 'teams/js/views/teams', - 'text!teams/templates/team-actions.underscore'], - function (Backbone, gettext, TeamsView, teamActionsTemplate) { + define([ + 'backbone', + 'gettext', + 'teams/js/views/teams', + 'common/js/components/views/paging_header', + 'text!teams/templates/team-actions.underscore' + ], function (Backbone, gettext, TeamsView, PagingHeader, teamActionsTemplate) { var TopicTeamsView = TeamsView.extend({ events: { 'click a.browse-teams': 'browseTeams', @@ -12,8 +15,8 @@ }, initialize: function(options) { + this.showSortControls = options.showSortControls; TeamsView.prototype.initialize.call(this, options); - _.bindAll(this, 'browseTeams', 'searchTeams', 'showCreateTeamForm'); }, render: function() { @@ -22,21 +25,29 @@ this.collection.refresh(), this.teamMemberships.refresh() ).done(function() { - TeamsView.prototype.render.call(self); + TeamsView.prototype.render.call(self); - if (self.teamMemberships.canUserCreateTeam()) { - var message = interpolate_text( - _.escape(gettext("Try {browse_span_start}browsing all teams{span_end} or {search_span_start}searching team descriptions{span_end}. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")), - { - 'browse_span_start': '<a class="browse-teams" href="">', - 'search_span_start': '<a class="search-teams" href="">', - 'create_span_start': '<a class="create-team" href="">', - 'span_end': '</a>' - } - ); - self.$el.append(_.template(teamActionsTemplate, {message: message})); - } - }); + if (self.teamMemberships.canUserCreateTeam()) { + var message = interpolate_text( + // Translators: this string is shown at the bottom of the teams page + // to find a team to join or else to create a new one. There are three + // links that need to be included in the message: + // 1. Browse teams in other topics + // 2. search teams + // 3. create a new team + // Be careful to start each link with the appropriate start indicator + // (e.g. {browse_span_start} for #1) and finish it with {span_end}. + _.escape(gettext("{browse_span_start}Browse teams in other topics{span_end} or {search_span_start}search teams{span_end} in this topic. If you still can't find a team to join, {create_span_start}create a new team in this topic{span_end}.")), + { + 'browse_span_start': '<a class="browse-teams" href="">', + 'search_span_start': '<a class="search-teams" href="">', + 'create_span_start': '<a class="create-team" href="">', + 'span_end': '</a>' + } + ); + self.$el.append(_.template(teamActionsTemplate, {message: message})); + } + }); return this; }, @@ -46,14 +57,29 @@ }, searchTeams: function (event) { + //var searchField = $('.page-header-search .search-field'); event.preventDefault(); + //searchField.focus(); + //searchField.select(); + //$('html, body').animate({ + // scrollTop: 0 + //}, 500); + // TODO! Will navigate to correct place once required functionality is available Backbone.history.navigate('browse', {trigger: true}); }, showCreateTeamForm: function (event) { event.preventDefault(); - Backbone.history.navigate('topics/' + this.teamParams.topicID + '/create-team', {trigger: true}); + Backbone.history.navigate('topics/' + this.model.id + '/create-team', {trigger: true}); + }, + + createHeaderView: function () { + return new PagingHeader({ + collection: this.options.collection, + srInfo: this.srInfo, + showSortControls: this.showSortControls + }); } }); diff --git a/lms/djangoapps/teams/static/teams/templates/edit-team.underscore b/lms/djangoapps/teams/static/teams/templates/edit-team.underscore index bea9c1a5dd..79fe69a4c1 100644 --- a/lms/djangoapps/teams/static/teams/templates/edit-team.underscore +++ b/lms/djangoapps/teams/static/teams/templates/edit-team.underscore @@ -27,6 +27,7 @@ <div class="team-edit-fields"> <div class="team-required-fields"> </div> + <div class="vertical-line"></div> <div class="team-optional-fields"> <fieldset> <div class="u-field u-field-optional_description"> @@ -43,25 +44,20 @@ <div class="create-team form-actions"> <button class="action action-primary"> - <%= - interpolate_text( - _.escape(gettext("{primaryButtonTitle} {span_start}a team{span_end}")), - { - 'primaryButtonTitle': primaryButtonTitle, 'span_start': '<span class="sr">', 'span_end': '</span>' - } - ) - %> + <span aria-hidden="true"><%- primaryButtonTitle %></span> + <% if (action === 'create') { %> + <span class="sr"><%- gettext("Create team.") %></span> + <% } else if (action === 'edit') { %> + <span class="sr"><%- gettext("Update team.") %></span> + <% } %> </button> <button class="action action-cancel"> - <%= - interpolate_text( - _.escape(gettext("Cancel {span_start} {action} team {span_end}")), - { - 'span_start': '<span class="sr">', 'span_end': '</span>', - 'action': action === 'create' ? 'creating' : 'updating' - } - ) - %> + <span aria-hidden="true"><%- gettext("Cancel") %></span> + <% if (action === 'create') { %> + <span class="sr"><%- gettext("Cancel team creating.") %></span> + <% } else if (action === 'edit') { %> + <span class="sr"><%- gettext("Cancel team updating.") %></span> + <% } %> </button> </div> </form> diff --git a/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore b/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore new file mode 100644 index 0000000000..ae91aef5f0 --- /dev/null +++ b/lms/djangoapps/teams/static/teams/templates/team-membership-details.underscore @@ -0,0 +1,9 @@ +<span class="member-count"><%= membership_message %></span> +<ul class="list-member-thumbs"> + <% _.each(memberships, function (membership) { %> + <li class="item-member-thumb"><img alt="<%- membership.user.username %>" src="<%- membership.user.profile_image.image_url_small %>"></img></li> + <% }) %> + <% if (has_additional_memberships) { %> + <li class="item-member-thumb"><span class="sr"><%- sr_message %></span>…</li> + <% } %> +</ul> diff --git a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore index d55ea1a946..36a689665a 100644 --- a/lms/djangoapps/teams/static/teams/templates/team-profile.underscore +++ b/lms/djangoapps/teams/static/teams/templates/team-profile.underscore @@ -5,7 +5,7 @@ data-user-create-comment="<%= !readOnly %>" data-user-create-subcomment="<%= !readOnly %>"> <% if ( !readOnly) { %> - <button type="button" class="btn new-post-btn"><i class="icon fa fa-edit new-post-icon" aria-hidden="true"></i><%= gettext("New Post") %></button> + <button type="button" class="btn new-post-btn"><i class="icon fa fa-edit new-post-icon" aria-hidden="true"></i><%- gettext("New Post") %></button> <% } %> </div> </div> diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html index d9b5995af9..60e0382eec 100644 --- a/lms/djangoapps/teams/templates/teams/teams.html +++ b/lms/djangoapps/teams/templates/teams/teams.html @@ -5,7 +5,7 @@ <%namespace name='static' file='/static_content.html'/> <%inherit file="/main.html" /> -<%block name="bodyclass">view-teams is-in-course course js</%block> +<%block name="bodyclass">view-teams view-in-course course js</%block> <%block name="pagetitle">${_("Teams")}</%block> <%block name="headextra"> diff --git a/lms/djangoapps/teams/tests/test_models.py b/lms/djangoapps/teams/tests/test_models.py index 39c645dd78..ba9fc6f53c 100644 --- a/lms/djangoapps/teams/tests/test_models.py +++ b/lms/djangoapps/teams/tests/test_models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=no-member """Tests for the teams API at the HTTP request level.""" from contextlib import contextmanager from datetime import datetime @@ -20,7 +21,7 @@ from django_comment_common.signals import ( ) from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from opaque_keys.edx.keys import CourseKey -from student.tests.factories import UserFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory from .factories import CourseTeamFactory, CourseTeamMembershipFactory from ..models import CourseTeam, CourseTeamMembership @@ -42,16 +43,18 @@ class TeamMembershipTest(SharedModuleStoreTestCase): self.user1 = UserFactory.create(username='user1') self.user2 = UserFactory.create(username='user2') + self.user3 = UserFactory.create(username='user3') + + for user in (self.user1, self.user2, self.user3): + CourseEnrollmentFactory.create(user=user, course_id=COURSE_KEY1) + CourseEnrollmentFactory.create(user=self.user1, course_id=COURSE_KEY2) self.team1 = CourseTeamFactory(course_id=COURSE_KEY1, team_id='team1') self.team2 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team2') - self.team_membership11 = CourseTeamMembership(user=self.user1, team=self.team1) - self.team_membership11.save() - self.team_membership12 = CourseTeamMembership(user=self.user2, team=self.team1) - self.team_membership12.save() - self.team_membership21 = CourseTeamMembership(user=self.user1, team=self.team2) - self.team_membership21.save() + self.team_membership11 = self.team1.add_user(self.user1) + self.team_membership12 = self.team1.add_user(self.user2) + self.team_membership21 = self.team2.add_user(self.user1) def test_membership_last_activity_set(self): current_last_activity = self.team_membership11.last_activity_at @@ -64,6 +67,24 @@ class TeamMembershipTest(SharedModuleStoreTestCase): # already exist. self.assertEqual(self.team_membership11.last_activity_at, current_last_activity) + def test_team_size_delete_membership(self): + """Test that the team size field is correctly updated when deleting a + team membership. + """ + self.assertEqual(self.team1.team_size, 2) + self.team_membership11.delete() + team = CourseTeam.objects.get(id=self.team1.id) + self.assertEqual(team.team_size, 1) + + def test_team_size_create_membership(self): + """Test that the team size field is correctly updated when creating a + team membership. + """ + self.assertEqual(self.team1.team_size, 2) + self.team1.add_user(self.user3) + team = CourseTeam.objects.get(id=self.team1.id) + self.assertEqual(team.team_size, 3) + @ddt.data( (None, None, None, 3), ('user1', None, None, 2), diff --git a/lms/djangoapps/teams/tests/test_views.py b/lms/djangoapps/teams/tests/test_views.py index a6af127651..07975309d7 100644 --- a/lms/djangoapps/teams/tests/test_views.py +++ b/lms/djangoapps/teams/tests/test_views.py @@ -14,6 +14,7 @@ from rest_framework.test import APITestCase, APIClient from courseware.tests.factories import StaffFactory from common.test.utils import skip_signal +from util.testing import EventTestMixin from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.models import CourseEnrollment from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -122,7 +123,7 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): 'id': 'topic_{}'.format(i), 'name': name, 'description': 'Description for topic {}.'.format(i) - } for i, name in enumerate([u'sólar power', 'Wind Power', 'Nuclear Power', 'Coal Power']) + } for i, name in enumerate([u'Sólar power', 'Wind Power', 'Nuclear Power', 'Coal Power']) ] } cls.test_course_1 = CourseFactory.create( @@ -201,26 +202,20 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_save_callback' ): - # 'solar team' is intentionally lower case to test case insensitivity in name ordering - self.test_team_1 = CourseTeamFactory.create( - name=u'sólar team', + self.solar_team = CourseTeamFactory.create( + name=u'Sólar team', course_id=self.test_course_1.id, topic_id='topic_0' ) - self.test_team_2 = CourseTeamFactory.create(name='Wind Team', course_id=self.test_course_1.id) - self.test_team_3 = CourseTeamFactory.create(name='Nuclear Team', course_id=self.test_course_1.id) - self.test_team_4 = CourseTeamFactory.create( - name='Coal Team', - course_id=self.test_course_1.id, - is_active=False - ) - self.test_team_5 = CourseTeamFactory.create(name='Another Team', course_id=self.test_course_2.id) - self.test_team_6 = CourseTeamFactory.create( + self.wind_team = CourseTeamFactory.create(name='Wind Team', course_id=self.test_course_1.id) + self.nuclear_team = CourseTeamFactory.create(name='Nuclear Team', course_id=self.test_course_1.id) + self.another_team = CourseTeamFactory.create(name='Another Team', course_id=self.test_course_2.id) + self.public_profile_team = CourseTeamFactory.create( name='Public Profile Team', course_id=self.test_course_2.id, topic_id='topic_6' ) - self.test_team_7 = CourseTeamFactory.create( + self.search_team = CourseTeamFactory.create( name='Search', description='queryable text', country='GS', @@ -230,13 +225,12 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): ) self.test_team_name_id_map = {team.name: team for team in ( - self.test_team_1, - self.test_team_2, - self.test_team_3, - self.test_team_4, - self.test_team_5, - self.test_team_6, - self.test_team_7, + self.solar_team, + self.wind_team, + self.nuclear_team, + self.another_team, + self.public_profile_team, + self.search_team, )} for user, course in [('staff', self.test_course_1), ('course_staff', self.test_course_1)]: @@ -244,10 +238,10 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): self.users[user], course.id, check_access=True ) - self.test_team_1.add_user(self.users['student_enrolled']) - self.test_team_3.add_user(self.users['student_enrolled_both_courses_other_team']) - self.test_team_5.add_user(self.users['student_enrolled_both_courses_other_team']) - self.test_team_6.add_user(self.users['student_enrolled_public_profile']) + self.solar_team.add_user(self.users['student_enrolled']) + self.nuclear_team.add_user(self.users['student_enrolled_both_courses_other_team']) + self.another_team.add_user(self.users['student_enrolled_both_courses_other_team']) + self.public_profile_team.add_user(self.users['student_enrolled_public_profile']) def build_membership_data_raw(self, username, team): """Assembles a membership creation payload based on the raw values provided.""" @@ -401,7 +395,7 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): def verify_expanded_team(self, team): """Verifies that fields exist on the returned team json indicating that it is expanded.""" - for field in ['id', 'name', 'is_active', 'course_id', 'topic_id', 'date_created', 'description']: + for field in ['id', 'name', 'course_id', 'topic_id', 'date_created', 'description']: self.assertIn(field, team) @@ -445,24 +439,21 @@ class TestListTeamsAPI(TeamAPITestCase): ) def test_filter_topic_id(self): - self.verify_names({'course_id': self.test_course_1.id, 'topic_id': 'topic_0'}, 200, [u'sólar team']) - - def test_filter_include_inactive(self): - self.verify_names({'include_inactive': True}, 200, ['Coal Team', 'Nuclear Team', u'sólar team', 'Wind Team']) + self.verify_names({'course_id': self.test_course_1.id, 'topic_id': 'topic_0'}, 200, [u'Sólar team']) @ddt.data( - (None, 200, ['Nuclear Team', u'sólar team', 'Wind Team']), - ('name', 200, ['Nuclear Team', u'sólar team', 'Wind Team']), - # Note that "Nuclear Team" and "solar team" have the same open_slots. - # "solar team" comes first due to secondary sort by last_activity_at. - ('open_slots', 200, ['Wind Team', u'sólar team', 'Nuclear Team']), + (None, 200, ['Nuclear Team', u'Sólar team', 'Wind Team']), + ('name', 200, ['Nuclear Team', u'Sólar team', 'Wind Team']), + # Note that "Nuclear Team" and "Solar team" have the same open_slots. + # "Solar team" comes first due to secondary sort by last_activity_at. + ('open_slots', 200, ['Wind Team', u'Sólar team', 'Nuclear Team']), # Note that "Wind Team" and "Nuclear Team" have the same last_activity_at. # "Wind Team" comes first due to secondary sort by open_slots. - ('last_activity_at', 200, [u'sólar team', 'Wind Team', 'Nuclear Team']), + ('last_activity_at', 200, [u'Sólar team', 'Wind Team', 'Nuclear Team']), ) @ddt.unpack def test_order_by(self, field, status, names): - # Make "solar team" the most recently active team. + # Make "Solar team" the most recently active team. # The CourseTeamFactory sets the last_activity_at to a fixed time (in the past), so all of the # other teams have the same last_activity_at. with skip_signal( @@ -471,7 +462,7 @@ class TestListTeamsAPI(TeamAPITestCase): sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_save_callback' ): - solar_team = self.test_team_name_id_map[u'sólar team'] + solar_team = self.test_team_name_id_map[u'Sólar team'] solar_team.last_activity_at = datetime.utcnow().replace(tzinfo=pytz.utc) solar_team.save() @@ -539,9 +530,12 @@ class TestListTeamsAPI(TeamAPITestCase): @ddt.ddt -class TestCreateTeamAPI(TeamAPITestCase): +class TestCreateTeamAPI(EventTestMixin, TeamAPITestCase): """Test cases for the team creation endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestCreateTeamAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -559,11 +553,15 @@ class TestCreateTeamAPI(TeamAPITestCase): teams = self.get_teams_list(user=user) self.assertIn("New Team", [team['name'] for team in teams['results']]) + def _expected_team_id(self, team, expected_prefix): + """ Return the team id that we'd expect given this team data and this prefix. """ + return expected_prefix + '-' + team['discussion_topic_id'] + def verify_expected_team_id(self, team, expected_prefix): """ Verifies that the team id starts with the specified prefix and ends with the discussion_topic_id """ self.assertIn('id', team) self.assertIn('discussion_topic_id', team) - self.assertEqual(team['id'], expected_prefix + '-' + team['discussion_topic_id']) + self.assertEqual(team['id'], self._expected_team_id(team, expected_prefix)) def test_naming(self): new_teams = [ @@ -612,7 +610,7 @@ class TestCreateTeamAPI(TeamAPITestCase): # First add the privileged user to a team. self.post_create_membership( 200, - self.build_membership_data(user, self.test_team_1), + self.build_membership_data(user, self.solar_team), user=user ) @@ -650,6 +648,19 @@ class TestCreateTeamAPI(TeamAPITestCase): self.verify_expected_team_id(team, 'fully-specified-team') del team['id'] + self.assert_event_emitted( + 'edx.team.created', + team_id=self._expected_team_id(team, 'fully-specified-team'), + course_id=unicode(self.test_course_1.id), + ) + + self.assert_event_emitted( + 'edx.team.learner_added', + team_id=self._expected_team_id(team, 'fully-specified-team'), + course_id=unicode(self.test_course_1.id), + user_id=self.users[creator].id, + add_method='added_on_create' + ) # Remove date_created and discussion_topic_id because they change between test runs del team['date_created'] del team['discussion_topic_id'] @@ -674,7 +685,6 @@ class TestCreateTeamAPI(TeamAPITestCase): 'name': 'Fully specified team', 'language': 'fr', 'country': 'CA', - 'is_active': True, 'topic_id': 'great-topic', 'course_id': str(self.test_course_1.id), 'description': 'Another fantastic team' @@ -708,10 +718,10 @@ class TestDetailTeamAPI(TeamAPITestCase): ) @ddt.unpack def test_access(self, user, status): - team = self.get_team_detail(self.test_team_1.team_id, status, user=user) + team = self.get_team_detail(self.solar_team.team_id, status, user=user) if status == 200: - self.assertEqual(team['description'], self.test_team_1.description) - self.assertEqual(team['discussion_topic_id'], self.test_team_1.discussion_topic_id) + self.assertEqual(team['description'], self.solar_team.description) + self.assertEqual(team['discussion_topic_id'], self.solar_team.discussion_topic_id) self.assertEqual(parser.parse(team['last_activity_at']), LAST_ACTIVITY_AT) def test_does_not_exist(self): @@ -719,12 +729,12 @@ class TestDetailTeamAPI(TeamAPITestCase): def test_expand_private_user(self): # Use the default user which is already private because to year_of_birth is set - result = self.get_team_detail(self.test_team_1.team_id, 200, {'expand': 'user'}) + result = self.get_team_detail(self.solar_team.team_id, 200, {'expand': 'user'}) self.verify_expanded_private_user(result['membership'][0]['user']) def test_expand_public_user(self): result = self.get_team_detail( - self.test_team_6.team_id, + self.public_profile_team.team_id, 200, {'expand': 'user'}, user='student_enrolled_public_profile' @@ -747,7 +757,7 @@ class TestUpdateTeamAPI(TeamAPITestCase): ) @ddt.unpack def test_access(self, user, status): - team = self.patch_team_detail(self.test_team_1.team_id, status, {'name': 'foo'}, user=user) + team = self.patch_team_detail(self.solar_team.team_id, status, {'name': 'foo'}, user=user) if status == 200: self.assertEquals(team['name'], 'foo') @@ -772,12 +782,12 @@ class TestUpdateTeamAPI(TeamAPITestCase): ) @ddt.unpack def test_bad_requests(self, key, value): - self.patch_team_detail(self.test_team_1.team_id, 400, {key: value}, user='staff') + self.patch_team_detail(self.solar_team.team_id, 400, {key: value}, user='staff') @ddt.data(('country', 'US'), ('language', 'en'), ('foo', 'bar')) @ddt.unpack def test_good_requests(self, key, value): - self.patch_team_detail(self.test_team_1.team_id, 200, {key: value}, user='staff') + self.patch_team_detail(self.solar_team.team_id, 200, {key: value}, user='staff') def test_does_not_exist(self): self.patch_team_detail('no_such_team', 404, user='staff') @@ -810,11 +820,11 @@ class TestListTopicsAPI(TeamAPITestCase): self.get_topics_list(400) @ddt.data( - (None, 200, ['Coal Power', 'Nuclear Power', u'sólar power', 'Wind Power'], 'name'), - ('name', 200, ['Coal Power', 'Nuclear Power', u'sólar power', 'Wind Power'], 'name'), - # Note that "Nuclear Power" and "solar power" both have 2 teams. "Coal Power" and "Window Power" + (None, 200, ['Coal Power', 'Nuclear Power', u'Sólar power', 'Wind Power'], 'name'), + ('name', 200, ['Coal Power', 'Nuclear Power', u'Sólar power', 'Wind Power'], 'name'), + # Note that "Nuclear Power" and "Solar power" both have 2 teams. "Coal Power" and "Window Power" # both have 0 teams. The secondary sort is alphabetical by name. - ('team_count', 200, ['Nuclear Power', u'sólar power', 'Coal Power', 'Wind Power'], 'team_count'), + ('team_count', 200, ['Nuclear Power', u'Sólar power', 'Coal Power', 'Wind Power'], 'team_count'), ('no_such_field', 400, [], None), ) @ddt.unpack @@ -865,7 +875,7 @@ class TestListTopicsAPI(TeamAPITestCase): 'page': 1, 'order_by': 'team_count' }) - self.assertEqual(["Wind Power", u'sólar power'], [topic['name'] for topic in topics['results']]) + self.assertEqual(["Wind Power", u'Sólar power'], [topic['name'] for topic in topics['results']]) topics = self.get_topics_list(data={ 'course_id': self.test_course_1.id, @@ -953,7 +963,7 @@ class TestListMembershipAPI(TeamAPITestCase): ) @ddt.unpack def test_access(self, user, status): - membership = self.get_membership_list(status, {'team_id': self.test_team_1.team_id}, user=user) + membership = self.get_membership_list(status, {'team_id': self.solar_team.team_id}, user=user) if status == 200: self.assertEqual(membership['count'], 1) self.assertEqual(membership['results'][0]['user']['username'], self.users['student_enrolled'].username) @@ -974,14 +984,14 @@ class TestListMembershipAPI(TeamAPITestCase): if status == 200: if has_content: self.assertEqual(membership['count'], 1) - self.assertEqual(membership['results'][0]['team']['team_id'], self.test_team_1.team_id) + self.assertEqual(membership['results'][0]['team']['team_id'], self.solar_team.team_id) else: self.assertEqual(membership['count'], 0) @ddt.data( ('student_enrolled_both_courses_other_team', 'TestX/TS101/Test_Course', 200, 'Nuclear Team'), ('student_enrolled_both_courses_other_team', 'MIT/6.002x/Circuits', 200, 'Another Team'), - ('student_enrolled', 'TestX/TS101/Test_Course', 200, u'sólar team'), + ('student_enrolled', 'TestX/TS101/Test_Course', 200, u'Sólar team'), ('student_enrolled', 'MIT/6.002x/Circuits', 400, ''), ) @ddt.unpack @@ -1004,10 +1014,10 @@ class TestListMembershipAPI(TeamAPITestCase): ) @ddt.unpack def test_course_filter_with_team_id(self, course_id, status): - membership = self.get_membership_list(status, {'team_id': self.test_team_1.team_id, 'course_id': course_id}) + membership = self.get_membership_list(status, {'team_id': self.solar_team.team_id, 'course_id': course_id}) if status == 200: self.assertEqual(membership['count'], 1) - self.assertEqual(membership['results'][0]['team']['team_id'], self.test_team_1.team_id) + self.assertEqual(membership['results'][0]['team']['team_id'], self.solar_team.team_id) def test_bad_course_id(self): self.get_membership_list(404, {'course_id': 'no_such_course'}) @@ -1020,26 +1030,29 @@ class TestListMembershipAPI(TeamAPITestCase): def test_expand_private_user(self): # Use the default user which is already private because to year_of_birth is set - result = self.get_membership_list(200, {'team_id': self.test_team_1.team_id, 'expand': 'user'}) + result = self.get_membership_list(200, {'team_id': self.solar_team.team_id, 'expand': 'user'}) self.verify_expanded_private_user(result['results'][0]['user']) def test_expand_public_user(self): result = self.get_membership_list( 200, - {'team_id': self.test_team_6.team_id, 'expand': 'user'}, + {'team_id': self.public_profile_team.team_id, 'expand': 'user'}, user='student_enrolled_public_profile' ) self.verify_expanded_public_user(result['results'][0]['user']) def test_expand_team(self): - result = self.get_membership_list(200, {'team_id': self.test_team_1.team_id, 'expand': 'team'}) + result = self.get_membership_list(200, {'team_id': self.solar_team.team_id, 'expand': 'team'}) self.verify_expanded_team(result['results'][0]['team']) @ddt.ddt -class TestCreateMembershipAPI(TeamAPITestCase): +class TestCreateMembershipAPI(EventTestMixin, TeamAPITestCase): """Test cases for the membership creation endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestCreateMembershipAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -1055,17 +1068,29 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_access(self, user, status): membership = self.post_create_membership( status, - self.build_membership_data('student_enrolled_not_on_team', self.test_team_1), + self.build_membership_data('student_enrolled_not_on_team', self.solar_team), user=user ) if status == 200: self.assertEqual(membership['user']['username'], self.users['student_enrolled_not_on_team'].username) - self.assertEqual(membership['team']['team_id'], self.test_team_1.team_id) - memberships = self.get_membership_list(200, {'team_id': self.test_team_1.team_id}) + self.assertEqual(membership['team']['team_id'], self.solar_team.team_id) + memberships = self.get_membership_list(200, {'team_id': self.solar_team.team_id}) self.assertEqual(memberships['count'], 2) + add_method = 'joined_from_team_view' if user == 'student_enrolled_not_on_team' else 'added_by_another_user' + + self.assert_event_emitted( + 'edx.team.learner_added', + team_id=self.solar_team.team_id, + user_id=self.users['student_enrolled_not_on_team'].id, + course_id=unicode(self.solar_team.course_id), + add_method=add_method + ) + else: + self.assert_no_events_were_emitted() + def test_no_username(self): - response = self.post_create_membership(400, {'team_id': self.test_team_1.team_id}) + response = self.post_create_membership(400, {'team_id': self.solar_team.team_id}) self.assertIn('username', json.loads(response.content)['field_errors']) def test_no_team(self): @@ -1081,7 +1106,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_bad_username(self): self.post_create_membership( 404, - self.build_membership_data_raw('no_such_user', self.test_team_1.team_id), + self.build_membership_data_raw('no_such_user', self.solar_team.team_id), user='staff' ) @@ -1089,7 +1114,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_join_twice(self, user): response = self.post_create_membership( 400, - self.build_membership_data('student_enrolled', self.test_team_1), + self.build_membership_data('student_enrolled', self.solar_team), user=user ) self.assertIn('already a member', json.loads(response.content)['developer_message']) @@ -1097,7 +1122,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_join_second_team_in_course(self): response = self.post_create_membership( 400, - self.build_membership_data('student_enrolled_both_courses_other_team', self.test_team_1), + self.build_membership_data('student_enrolled_both_courses_other_team', self.solar_team), user='student_enrolled_both_courses_other_team' ) self.assertIn('already a member', json.loads(response.content)['developer_message']) @@ -1106,7 +1131,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_not_enrolled_in_team_course(self, user): response = self.post_create_membership( 400, - self.build_membership_data('student_unenrolled', self.test_team_1), + self.build_membership_data('student_unenrolled', self.solar_team), user=user ) self.assertIn('not enrolled', json.loads(response.content)['developer_message']) @@ -1114,7 +1139,7 @@ class TestCreateMembershipAPI(TeamAPITestCase): def test_over_max_team_size_in_course_2(self): response = self.post_create_membership( 400, - self.build_membership_data('student_enrolled_other_course_not_on_team', self.test_team_5), + self.build_membership_data('student_enrolled_other_course_not_on_team', self.another_team), user='student_enrolled_other_course_not_on_team' ) self.assertIn('full', json.loads(response.content)['developer_message']) @@ -1137,7 +1162,7 @@ class TestDetailMembershipAPI(TeamAPITestCase): @ddt.unpack def test_access(self, user, status): self.get_membership_detail( - self.test_team_1.team_id, + self.solar_team.team_id, self.users['student_enrolled'].username, status, user=user @@ -1147,11 +1172,11 @@ class TestDetailMembershipAPI(TeamAPITestCase): self.get_membership_detail('no_such_team', self.users['student_enrolled'].username, 404) def test_bad_username(self): - self.get_membership_detail(self.test_team_1.team_id, 'no_such_user', 404) + self.get_membership_detail(self.solar_team.team_id, 'no_such_user', 404) def test_no_membership(self): self.get_membership_detail( - self.test_team_1.team_id, + self.solar_team.team_id, self.users['student_enrolled_not_on_team'].username, 404 ) @@ -1159,7 +1184,7 @@ class TestDetailMembershipAPI(TeamAPITestCase): def test_expand_private_user(self): # Use the default user which is already private because to year_of_birth is set result = self.get_membership_detail( - self.test_team_1.team_id, + self.solar_team.team_id, self.users['student_enrolled'].username, 200, {'expand': 'user'} @@ -1168,7 +1193,7 @@ class TestDetailMembershipAPI(TeamAPITestCase): def test_expand_public_user(self): result = self.get_membership_detail( - self.test_team_6.team_id, + self.public_profile_team.team_id, self.users['student_enrolled_public_profile'].username, 200, {'expand': 'user'}, @@ -1178,7 +1203,7 @@ class TestDetailMembershipAPI(TeamAPITestCase): def test_expand_team(self): result = self.get_membership_detail( - self.test_team_1.team_id, + self.solar_team.team_id, self.users['student_enrolled'].username, 200, {'expand': 'team'} @@ -1187,9 +1212,12 @@ class TestDetailMembershipAPI(TeamAPITestCase): @ddt.ddt -class TestDeleteMembershipAPI(TeamAPITestCase): +class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase): """Test cases for the membership deletion endpoint.""" + def setUp(self): # pylint: disable=arguments-differ + super(TestDeleteMembershipAPI, self).setUp('teams.views.tracker') + @ddt.data( (None, 401), ('student_inactive', 401), @@ -1203,17 +1231,29 @@ class TestDeleteMembershipAPI(TeamAPITestCase): @ddt.unpack def test_access(self, user, status): self.delete_membership( - self.test_team_1.team_id, + self.solar_team.team_id, self.users['student_enrolled'].username, status, user=user ) + if status == 204: + remove_method = 'self_removal' if user == 'student_enrolled' else 'removed_by_admin' + self.assert_event_emitted( + 'edx.team.learner_removed', + team_id=self.solar_team.team_id, + course_id=unicode(self.solar_team.course_id), + user_id=self.users['student_enrolled'].id, + remove_method=remove_method + ) + else: + self.assert_no_events_were_emitted() + def test_bad_team(self): self.delete_membership('no_such_team', self.users['student_enrolled'].username, 404) def test_bad_username(self): - self.delete_membership(self.test_team_1.team_id, 'no_such_user', 404) + self.delete_membership(self.solar_team.team_id, 'no_such_user', 404) def test_missing_membership(self): - self.delete_membership(self.test_team_2.team_id, self.users['student_enrolled'].username, 404) + self.delete_membership(self.wind_team.team_id, self.users['student_enrolled'].username, 404) diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index 4b8692e4b6..6d46ac68f2 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -34,6 +34,7 @@ from xmodule.modulestore.django import modulestore from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from eventtracking import tracker from courseware.courses import get_course_with_access, has_access from student.models import CourseEnrollment, CourseAccessRole from student.roles import CourseStaffRole @@ -51,7 +52,7 @@ from .serializers import ( add_team_count ) from .search_indexes import CourseTeamIndexer -from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam +from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam, ElasticSearchConnectionError TEAM_MEMBERSHIPS_PER_PAGE = 2 TOPICS_PER_PAGE = 12 @@ -97,7 +98,7 @@ class TeamsDashboardView(View): team_memberships_page = Paginator(team_memberships, TEAM_MEMBERSHIPS_PER_PAGE).page(1) team_memberships_serializer = PaginatedMembershipSerializer( instance=team_memberships_page, - context={'expand': ('team',)}, + context={'expand': ('team', 'user'), 'request': request}, ) context = { @@ -120,7 +121,7 @@ class TeamsDashboardView(View): "teams_detail_url": reverse('teams_detail', args=['team_id']), "team_memberships_url": reverse('team_membership_list', request=request), "team_membership_detail_url": reverse('team_membership_detail', args=['team_id', user.username]), - "languages": settings.ALL_LANGUAGES, + "languages": [[lang[0], _(lang[1])] for lang in settings.ALL_LANGUAGES], # pylint: disable=translation-of-non-string "countries": list(countries), "disable_courseware_js": True, "teams_base_url": reverse('teams_dashboard', request=request, kwargs={'course_id': course_id}), @@ -175,7 +176,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): * text_search: Searches for full word matches on the name, description, country, and language fields. NOTES: Search is on full names for countries and languages, not the ISO codes. Text_search cannot be requested along with - with order_by. Searching relies on the ENABLE_TEAMS_SEARCH flag being set to True. + with order_by. * order_by: Cannot be called along with with text_search. Must be one of the following: @@ -191,9 +192,6 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): * page: Page number to retrieve. - * include_inactive: If true, inactive teams will be returned. The - default is to not include inactive teams. - * expand: Comma separated list of types for which to return expanded representations. Supports "user" and "team". @@ -220,10 +218,6 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): * name: The name of the team. - * is_active: True if the team is currently active. If false, the - team is considered "soft deleted" and will not be included by - default in results. - * course_id: The identifier for the course this team belongs to. * topic_id: Optionally specifies which topic the team is associated @@ -266,8 +260,8 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): Any logged in user who has verified their email address can create a team. The format mirrors that of a GET for an individual team, - but does not include the id, is_active, date_created, or membership - fields. id is automatically computed based on name. + but does not include the id, date_created, or membership fields. + id is automatically computed based on name. If the user is not logged in, a 401 error is returned. @@ -292,9 +286,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): def get(self, request): """GET /api/team/v0/teams/""" - result_filter = { - 'is_active': True - } + result_filter = {} if 'course_id' in request.QUERY_PARAMS: course_id_string = request.QUERY_PARAMS['course_id'] @@ -320,7 +312,8 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): status=status.HTTP_400_BAD_REQUEST ) - if 'text_search' in request.QUERY_PARAMS and 'order_by' in request.QUERY_PARAMS: + text_search = request.QUERY_PARAMS.get('text_search', None) + if text_search and request.QUERY_PARAMS.get('order_by', None): return Response( build_api_error(ugettext_noop("text_search and order_by cannot be provided together")), status=status.HTTP_400_BAD_REQUEST @@ -335,16 +328,19 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ) return Response(error, status=status.HTTP_400_BAD_REQUEST) result_filter.update({'topic_id': request.QUERY_PARAMS['topic_id']}) - if 'include_inactive' in request.QUERY_PARAMS and request.QUERY_PARAMS['include_inactive'].lower() == 'true': - del result_filter['is_active'] - if 'text_search' in request.QUERY_PARAMS and CourseTeamIndexer.search_is_enabled(): - search_engine = CourseTeamIndexer.engine() - text_search = request.QUERY_PARAMS['text_search'].encode('utf-8') + if text_search and CourseTeamIndexer.search_is_enabled(): + try: + search_engine = CourseTeamIndexer.engine() + except ElasticSearchConnectionError: + return Response( + build_api_error(ugettext_noop('Error connecting to elasticsearch')), + status=status.HTTP_400_BAD_REQUEST + ) result_filter.update({'course_id': course_id_string}) search_results = search_engine.search( - query_string=text_search, + query_string=text_search.encode('utf-8'), field_dictionary=result_filter, size=MAXIMUM_SEARCH_SIZE, ) @@ -355,19 +351,16 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): self.get_paginate_by(), self.get_page() ) - serializer = self.get_pagination_serializer(paginated_results) else: queryset = CourseTeam.objects.filter(**result_filter) order_by_input = request.QUERY_PARAMS.get('order_by', 'name') if order_by_input == 'name': - queryset = queryset.extra(select={'lower_name': "lower(name)"}) - queryset = queryset.order_by('lower_name') + # MySQL does case-insensitive order_by. + queryset = queryset.order_by('name') elif order_by_input == 'open_slots': - queryset = queryset.annotate(team_size=Count('users')) queryset = queryset.order_by('team_size', '-last_activity_at') elif order_by_input == 'last_activity_at': - queryset = queryset.annotate(team_size=Count('users')) queryset = queryset.order_by('-last_activity_at', 'team_size') else: return Response({ @@ -431,9 +424,22 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): }, status=status.HTTP_400_BAD_REQUEST) else: team = serializer.save() + tracker.emit('edx.team.created', { + 'team_id': team.team_id, + 'course_id': unicode(course_id) + }) if not team_administrator: # Add the creating user to the team. team.add_user(request.user) + tracker.emit( + 'edx.team.learner_added', + { + 'team_id': team.team_id, + 'user_id': request.user.id, + 'course_id': unicode(team.course_id), + 'add_method': 'added_on_create' + } + ) return Response(CourseTeamSerializer(team).data) def get_page(self): @@ -496,10 +502,6 @@ class TeamsDetailView(ExpandableFieldViewMixin, RetrievePatchAPIView): * name: The name of the team. - * is_active: True if the team is currently active. If false, the team - is considered "soft deleted" and will not be included by default in - results. - * course_id: The identifier for the course this team belongs to. * topic_id: Optionally specifies which topic the team is @@ -992,6 +994,15 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): try: membership = team.add_user(user) + tracker.emit( + 'edx.team.learner_added', + { + 'team_id': team.team_id, + 'user_id': user.id, + 'course_id': unicode(team.course_id), + 'add_method': 'joined_from_team_view' if user == request.user else 'added_by_another_user' + } + ) except AlreadyOnTeamInCourse: return Response( build_api_error( @@ -1118,6 +1129,15 @@ class MembershipDetailView(ExpandableFieldViewMixin, GenericAPIView): if has_team_api_access(request.user, team.course_id, access_username=username): membership = self.get_membership(username, team) membership.delete() + tracker.emit( + 'edx.team.learner_removed', + { + 'team_id': team.team_id, + 'course_id': unicode(team.course_id), + 'user_id': membership.user.id, + 'remove_method': 'self_removal' if membership.user == request.user else 'removed_by_admin' + } + ) return Response(status=status.HTTP_204_NO_CONTENT) else: return Response(status=status.HTTP_404_NOT_FOUND) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 50d593591b..aae22b3f82 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -552,6 +552,7 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): 'social.backends.linkedin.LinkedinOAuth2', 'social.backends.facebook.FacebookOAuth2', 'third_party_auth.saml.SAMLAuthBackend', + 'third_party_auth.lti.LTIAuthBackend', ]) + list(AUTHENTICATION_BACKENDS) ) @@ -566,6 +567,7 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', '') SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = AUTH_TOKENS.get('SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', '') SOCIAL_AUTH_OAUTH_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_OAUTH_SECRETS', {}) + SOCIAL_AUTH_LTI_CONSUMER_SECRETS = AUTH_TOKENS.get('SOCIAL_AUTH_LTI_CONSUMER_SECRETS', {}) # third_party_auth config moved to ConfigurationModels. This is for data migration only: THIRD_PARTY_AUTH_OLD_CONFIG = AUTH_TOKENS.get('THIRD_PARTY_AUTH', None) @@ -690,8 +692,13 @@ CREDIT_PROVIDER_SECRET_KEYS = AUTH_TOKENS.get("CREDIT_PROVIDER_SECRET_KEYS", {}) if FEATURES.get('ENABLE_LTI_PROVIDER'): INSTALLED_APPS += ('lti_provider',) AUTHENTICATION_BACKENDS += ('lti_provider.users.LtiBackend', ) + LTI_USER_EMAIL_DOMAIN = ENV_TOKENS.get('LTI_USER_EMAIL_DOMAIN', 'lti.example.com') +# For more info on this, see the notes in common.py +LTI_AGGREGATE_SCORE_PASSBACK_DELAY = ENV_TOKENS.get( + 'LTI_AGGREGATE_SCORE_PASSBACK_DELAY', LTI_AGGREGATE_SCORE_PASSBACK_DELAY +) ##################### Credit Provider help link #################### CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_URL) diff --git a/lms/envs/common.py b/lms/envs/common.py index f5c0d4d2e8..c1ebcfbb04 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -395,15 +395,15 @@ FEATURES = { # Course discovery feature 'ENABLE_COURSE_DISCOVERY': False, + # Setting for overriding default filtering facets for Course discovery + # COURSE_DISCOVERY_FILTERS = ["org", "language", "modes"] + # Software secure fake page feature flag 'ENABLE_SOFTWARE_SECURE_FAKE': False, # Teams feature 'ENABLE_TEAMS': True, - # Enable indexing teams for search - 'ENABLE_TEAMS_SEARCH': False, - # Show video bumper in LMS 'ENABLE_VIDEO_BUMPER': False, @@ -1518,6 +1518,7 @@ PIPELINE_JS = { 'source_filenames': ['js/xblock/core.js'] + sorted(common_js) + sorted(project_js) + base_application_js + [ 'js/sticky_filter.js', 'js/query-params.js', + 'js/vendor/moment.min.js', ], 'output_filename': 'js/lms-application.js', }, @@ -2635,6 +2636,18 @@ CREDIT_HELP_LINK_URL = "#" # route any messages intended for LTI users to a common domain. LTI_USER_EMAIL_DOMAIN = 'lti.example.com' +# An aggregate score is one derived from multiple problems (such as the +# cumulative score for a vertical element containing many problems). Sending +# aggregate scores immediately introduces two issues: one is a race condition +# between the view method and the Celery task where the updated score may not +# yet be visible to the database if the view has not yet returned (and committed +# its transaction). The other is that the student is likely to receive a stream +# of notifications as the score is updated with every problem. Waiting a +# reasonable period of time allows the view transaction to end, and allows us to +# collapse multiple score updates into a single message. +# The time value is in seconds. +LTI_AGGREGATE_SCORE_PASSBACK_DELAY = 15 * 60 + # Number of seconds before JWT tokens expire JWT_EXPIRATION = 30 JWT_ISSUER = None diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 3d90ef36a9..8d3fba9362 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -138,7 +138,7 @@ FEATURES['LICENSING'] = True ########################## Courseware Search ####################### -FEATURES['ENABLE_COURSEWARE_SEARCH'] = False +FEATURES['ENABLE_COURSEWARE_SEARCH'] = True SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" @@ -167,7 +167,9 @@ COURSE_DISCOVERY_MEANINGS = { 'language': LANGUAGE_MAP, } -FEATURES['ENABLE_COURSE_DISCOVERY'] = False +FEATURES['ENABLE_COURSE_DISCOVERY'] = True +# Setting for overriding default filtering facets for Course discovery +# COURSE_DISCOVERY_FILTERS = ["org", "language", "modes"] FEATURES['COURSES_ARE_BROWSEABLE'] = True HOMEPAGE_COURSE_MAX = 9 diff --git a/lms/envs/test.py b/lms/envs/test.py index f237a9af23..b26c03b4de 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -250,6 +250,7 @@ AUTHENTICATION_BACKENDS = ( 'social.backends.twitter.TwitterOAuth', 'third_party_auth.dummy.DummyBackend', 'third_party_auth.saml.SAMLAuthBackend', + 'third_party_auth.lti.LTIAuthBackend', ) + AUTHENTICATION_BACKENDS ################################## OPENID ##################################### @@ -485,7 +486,7 @@ FEATURES['ENABLE_EDXNOTES'] = True # Enable teams feature for tests. FEATURES['ENABLE_TEAMS'] = True -# Enable indexing teams for search +# Enable teams search for tests. FEATURES['ENABLE_TEAMS_SEARCH'] = True # Add milestones to Installed apps for testing diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index 85badfc316..55e5f40562 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -22,6 +22,8 @@ class DataDownload @$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'") @$list_proctored_exam_results_csv_btn = @$section.find("input[name='proctored-exam-results-report']'") @$list_may_enroll_csv_btn = @$section.find("input[name='list-may-enroll-csv']") + @$list_problem_responses_csv_input = @$section.find("input[name='problem-location']") + @$list_problem_responses_csv_btn = @$section.find("input[name='list-problem-responses-csv']") @$list_anon_btn = @$section.find("input[name='list-anon-ids']'") @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") @@ -117,6 +119,22 @@ class DataDownload grid = new Slick.Grid($table_placeholder, grid_data, columns, options) # grid.autosizeColumns() + @$list_problem_responses_csv_btn.click (e) => + @clear_display() + + url = @$list_problem_responses_csv_btn.data 'endpoint' + $.ajax + dataType: 'json' + url: url + data: + problem_location: @$list_problem_responses_csv_input.val() + error: (std_ajax_err) => + @$reports_request_response_error.text JSON.parse(std_ajax_err['responseText']) + $(".msg-error").css({"display":"block"}) + success: (data) => + @$reports_request_response.text data['status'] + $(".msg-confirm").css({"display":"block"}) + @$list_may_enroll_csv_btn.click (e) => @clear_display() diff --git a/lms/static/js/components/header/views/header.js b/lms/static/js/components/header/views/header.js index 73910aefb1..1686040c27 100644 --- a/lms/static/js/components/header/views/header.js +++ b/lms/static/js/components/header/views/header.js @@ -17,7 +17,7 @@ var json = this.model.attributes; this.$el.html(this.template(json)); if (this.headerActionsView) { - this.headerActionsView.setElement(this.$('.header-action-view')).render(); + this.headerActionsView.setElement(this.$('.page-header-secondary')).render(); } return this; } diff --git a/lms/static/js/spec/search/search_spec.js b/lms/static/js/spec/search/search_spec.js index 6dce51fe99..e23edc94a3 100644 --- a/lms/static/js/spec/search/search_spec.js +++ b/lms/static/js/spec/search/search_spec.js @@ -1,8 +1,8 @@ define([ 'jquery', - 'sinon', 'backbone', 'logger', + 'common/js/spec_helpers/ajax_helpers', 'common/js/spec_helpers/template_helpers', 'js/search/base/models/search_result', 'js/search/base/collections/search_collection', @@ -17,9 +17,9 @@ define([ 'js/search/dashboard/dashboard_search_factory' ], function( $, - Sinon, Backbone, Logger, + AjaxHelpers, TemplateHelpers, SearchResult, SearchCollection, @@ -51,11 +51,10 @@ define([ }); - // TODO: fix and re-enable. See SOL-1065 - xdescribe('SearchCollection', function () { + + describe('SearchCollection', function () { beforeEach(function () { - this.server = Sinon.fakeServer.create(); this.collection = new SearchCollection(); this.onSearch = jasmine.createSpy('onSearch'); @@ -68,23 +67,22 @@ define([ this.collection.on('error', this.onError); }); - afterEach(function () { - this.server.restore(); - }); - it('sends a request without a course ID', function () { var collection = new SearchCollection([]); + spyOn($, 'ajax'); collection.performSearch('search string'); - expect(this.server.requests[0].url).toEqual('/search/'); + expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/'); }); it('sends a request with course ID', function () { var collection = new SearchCollection([], { courseId: 'edx101' }); + spyOn($, 'ajax'); collection.performSearch('search string'); - expect(this.server.requests[0].url).toEqual('/search/edx101'); + expect($.ajax.mostRecentCall.args[0].url).toEqual('/search/edx101'); }); it('sends a request and parses the json result', function () { + var requests = AjaxHelpers.requests(this); this.collection.performSearch('search string'); var response = { total: 2, @@ -98,8 +96,7 @@ define([ } }] }; - this.server.respondWith('POST', this.collection.url, [200, {}, JSON.stringify(response)]); - this.server.respond(); + AjaxHelpers.respondWithJson(requests, response); expect(this.onSearch).toHaveBeenCalled(); expect(this.collection.totalCount).toEqual(1); @@ -110,28 +107,30 @@ define([ }); it('handles errors', function () { + var requests = AjaxHelpers.requests(this); this.collection.performSearch('search string'); - this.server.respond(); + AjaxHelpers.respondWithError(requests, 500); expect(this.onSearch).not.toHaveBeenCalled(); expect(this.onError).toHaveBeenCalled(); }); it('loads next page', function () { + var requests = AjaxHelpers.requests(this); var response = { total: 35, results: [] }; this.collection.loadNextPage(); - this.server.respond('POST', this.collection.url, [200, {}, JSON.stringify(response)]); + AjaxHelpers.respondWithJson(requests, response); expect(this.onNext).toHaveBeenCalled(); expect(this.onError).not.toHaveBeenCalled(); }); it('sends correct paging parameters', function () { + var requests = AjaxHelpers.requests(this); var searchString = 'search string'; var response = { total: 52, results: [] }; this.collection.performSearch(searchString); - this.server.respondWith('POST', this.collection.url, [200, {}, JSON.stringify(response)]); - this.server.respond(); + AjaxHelpers.respondWithJson(requests, response); this.collection.loadNextPage(); - this.server.respond(); + AjaxHelpers.respondWithJson(requests, response); spyOn($, 'ajax'); this.collection.loadNextPage(); expect($.ajax.mostRecentCall.args[0].url).toEqual(this.collection.url); @@ -141,31 +140,33 @@ define([ }); it('has next page', function () { + var requests = AjaxHelpers.requests(this); var response = { total: 35, access_denied_count: 5, results: [] }; this.collection.performSearch('search string'); - this.server.respond('POST', this.collection.url, [200, {}, JSON.stringify(response)]); + AjaxHelpers.respondWithJson(requests, response); expect(this.collection.hasNextPage()).toEqual(true); this.collection.loadNextPage(); - this.server.respond(); + AjaxHelpers.respondWithJson(requests, response); expect(this.collection.hasNextPage()).toEqual(false); }); it('aborts any previous request', function () { + var requests = AjaxHelpers.requests(this); var response = { total: 35, results: [] }; this.collection.performSearch('old search'); this.collection.performSearch('new search'); - this.server.respond('POST', this.collection.url, [200, {}, JSON.stringify(response)]); + AjaxHelpers.respondWithJson(requests, response); expect(this.onSearch.calls.length).toEqual(1); this.collection.performSearch('old search'); this.collection.cancelSearch(); - this.server.respond('POST', this.collection.url, [200, {}, JSON.stringify(response)]); + AjaxHelpers.respondWithJson(requests, response); expect(this.onSearch.calls.length).toEqual(1); this.collection.loadNextPage(); this.collection.loadNextPage(); - this.server.respond('POST', this.collection.url, [200, {}, JSON.stringify(response)]); + AjaxHelpers.respondWithJson(requests, response); expect(this.onNext.calls.length).toEqual(1); }); @@ -558,9 +559,10 @@ define([ } function performsSearch () { + var requests = AjaxHelpers.requests(this); $('.search-field').val('search string'); $('.search-button').trigger('click'); - this.server.respondWith([200, {}, JSON.stringify({ + AjaxHelpers.respondWithJson(requests, { total: 1337, access_denied_count: 12, results: [{ @@ -572,19 +574,18 @@ define([ course_name: '' } }] - })]); - this.server.respond(); + }); expect($('.search-info')).toExist(); expect($('.search-result-list')).toBeVisible(); expect(this.$searchResults.find('li').length).toEqual(1); } function showsErrorMessage () { + var requests = AjaxHelpers.requests(this); $('.search-field').val('search string'); $('.search-button').trigger('click'); - this.server.respondWith([500, {}]); - this.server.respond(); - expect(this.$searchResults).toEqual($('#search_error-tpl')); + AjaxHelpers.respondWithError(requests, 500, {}); + expect(this.$searchResults).toContainHtml('There was an error'); } function updatesNavigationHistory () { @@ -596,12 +597,13 @@ define([ } function cancelsSearchRequest () { + var requests = AjaxHelpers.requests(this); // send search request to server $('.search-field').val('search string'); $('.search-button').trigger('click'); // cancel search $('.cancel-button').trigger('click'); - this.server.respondWith([200, {}, JSON.stringify({ + AjaxHelpers.respondWithJson(requests, { total: 1337, access_denied_count: 12, results: [{ @@ -613,8 +615,7 @@ define([ course_name: '' } }] - })]); - this.server.respond(); + }); // there should be no results expect(this.$contentElement).toBeVisible(); expect(this.$searchResults).toBeHidden(); @@ -627,9 +628,8 @@ define([ } function loadsNextPage () { - $('.search-field').val('query'); - $('.search-button').trigger('click'); - this.server.respondWith([200, {}, JSON.stringify({ + var requests = AjaxHelpers.requests(this); + var response = { total: 1337, access_denied_count: 12, results: [{ @@ -641,21 +641,24 @@ define([ course_name: '' } }] - })]); - this.server.respond(); + }; + $('.search-field').val('query'); + $('.search-button').trigger('click'); + AjaxHelpers.respondWithJson(requests, response); expect(this.$searchResults.find('li').length).toEqual(1); expect($('.search-load-next')).toBeVisible(); $('.search-load-next').trigger('click'); - var body = this.server.requests[1].requestBody; + var body = requests[1].requestBody; expect(body).toContain('search_string=query'); expect(body).toContain('page_index=1'); - this.server.respond(); + AjaxHelpers.respondWithJson(requests, response); expect(this.$searchResults.find('li').length).toEqual(2); } function navigatesToSearch () { + var requests = AjaxHelpers.requests(this); Backbone.history.loadUrl('search/query'); - expect(this.server.requests[0].requestBody).toContain('search_string=query'); + expect(requests[0].requestBody).toContain('search_string=query'); } function loadTemplates () { @@ -679,7 +682,6 @@ define([ ); loadTemplates.call(this); - this.server = Sinon.fakeServer.create(); var courseId = 'a/b/c'; CourseSearchFactory(courseId); spyOn(Backbone.history, 'navigate'); @@ -687,12 +689,9 @@ define([ this.$searchResults = $('#courseware-search-results'); }); - afterEach(function () { - this.server.restore(); - }); - it('shows loading message on search', showsLoadingMessage); it('performs search', performsSearch); + it('shows an error message', showsErrorMessage); it('updates navigation history', updatesNavigationHistory); it('cancels search request', cancelsSearchRequest); it('clears results', clearsResults); @@ -710,8 +709,6 @@ define([ '<section id="my-courses"></section>' ); loadTemplates.call(this); - - this.server = Sinon.fakeServer.create(); DashboardSearchFactory(); spyOn(Backbone.history, 'navigate'); @@ -719,21 +716,19 @@ define([ this.$searchResults = $('#dashboard-search-results'); }); - afterEach(function () { - this.server.restore(); - }); - it('shows loading message on search', showsLoadingMessage); it('performs search', performsSearch); + it('shows an error message', showsErrorMessage); it('updates navigation history', updatesNavigationHistory); it('cancels search request', cancelsSearchRequest); it('clears results', clearsResults); it('loads next page', loadsNextPage); it('navigates to search', navigatesToSearch); it('returns to course list', function () { + var requests = AjaxHelpers.requests(this); $('.search-field').val('search string'); $('.search-button').trigger('click'); - this.server.respondWith([200, {}, JSON.stringify({ + AjaxHelpers.respondWithJson(requests, { total: 1337, access_denied_count: 12, results: [{ @@ -745,8 +740,7 @@ define([ course_name: '' } }] - })]); - this.server.respond(); + }); expect($('.search-back-to-courses')).toExist(); $('.search-back-to-courses').trigger('click'); expect(this.$contentElement).toBeVisible(); diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js index 44204a5781..12cdda51f5 100644 --- a/lms/static/js/spec/student_account/account_settings_factory_spec.js +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -35,6 +35,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers 'id': 'oa2-network1', 'name': "Network1", 'connected': true, + 'accepts_logins': 'true', 'connect_url': 'yetanother1.com/auth/connect', 'disconnect_url': 'yetanother1.com/auth/disconnect' }, @@ -42,6 +43,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers 'id': 'oa2-network2', 'name': "Network2", 'connected': true, + 'accepts_logins': 'true', 'connect_url': 'yetanother2.com/auth/connect', 'disconnect_url': 'yetanother2.com/auth/disconnect' } diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js index c924872f82..9d5b95616b 100644 --- a/lms/static/js/spec/student_account/account_settings_fields_spec.js +++ b/lms/static/js/spec/student_account/account_settings_fields_spec.js @@ -111,6 +111,7 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers helpMessage: '', valueAttribute: 'auth-yet-another', connected: true, + acceptsLogins: 'true', connectUrl: 'yetanother.com/auth/connect', disconnectUrl: 'yetanother.com/auth/disconnect' }); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 706f3a0efb..dc3fa09be8 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -149,6 +149,7 @@ helpMessage: '', connected: provider.connected, connectUrl: provider.connect_url, + acceptsLogins: provider.accepts_logins, disconnectUrl: provider.disconnect_url }) }; diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js index f20f7bff4a..e2cf8e3e26 100644 --- a/lms/static/js/student_account/views/account_settings_fields.js +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -116,11 +116,20 @@ }, render: function () { + var linkTitle; + if (this.options.connected) { + linkTitle = gettext('Unlink'); + } else if (this.options.acceptsLogins) { + linkTitle = gettext('Link') + } else { + linkTitle = '' + } + this.$el.html(this.template({ id: this.options.valueAttribute, title: this.options.title, screenReaderTitle: this.options.screenReaderTitle, - linkTitle: this.options.connected ? gettext('Unlink') : gettext('Link'), + linkTitle: linkTitle, linkHref: '', message: this.helpMessage })); diff --git a/lms/static/lms/js/require-config.js b/lms/static/lms/js/require-config.js index a40ac7f362..04fd904825 100644 --- a/lms/static/lms/js/require-config.js +++ b/lms/static/lms/js/require-config.js @@ -185,8 +185,11 @@ }, "tinymce": { exports: "tinymce" - } + }, // End of needed by OVA + "moment": { + exports: "moment" + } } }); }).call(this, require || RequireJS.require, define || RequireJS.define); diff --git a/lms/static/sass/base/_layouts.scss b/lms/static/sass/base/_layouts.scss index c828111589..6694e23db0 100644 --- a/lms/static/sass/base/_layouts.scss +++ b/lms/static/sass/base/_layouts.scss @@ -3,8 +3,7 @@ // overriding existing styles on the body element // .view-incourse scopes these rules to be specific to student being in a course -body.view-incourse, -body.is-in-course { +body.view-in-course { background-color: $body-bg; // keep application of widths to window-wrap @@ -59,12 +58,17 @@ body.is-in-course { .profile-wrapper, .instructor-dashboard-wrapper-2, .wiki-wrapper, - .teams-wrapper { + .teams-wrapper, + .static_tab_wrapper { max-width: 1180px; margin: 0 auto; padding: 0; } + .static_tab_wrapper { + padding: 2em 2.5em; + } + // post-container footer (creative commons) .container-footer { max-width: none; @@ -81,12 +85,16 @@ body.is-in-course { // site footer .wrapper-footer { - margin-top: $baseline; + margin-top: ($baseline*2); padding-right: 2%; padding-left: 2%; - footer#footer-openedx { // TODO check edX footer when it launches + footer#footer-openedx { // shame selector to match existing min-width: auto; } } + + footer#footer-edx-v3 { // shame selector to match existing + margin-top: ($baseline*2); + } } diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index cff6ca5dfe..df043d97d9 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -1588,7 +1588,6 @@ input[name="subject"] { font-size: 12px; color: #646464 } - top: 100px !important; width: 650px; @include margin-left(-325px); border-radius: 2px; diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index 6608f96b83..2e78ce3541 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -5,6 +5,7 @@ .wrapper-footer { @extend %ui-print-excluded; + margin-top: ($baseline*2); box-shadow: 0 -1px 5px 0 $shadow-l1; border-top: 1px solid tint($m-gray, 50%); padding: 25px ($baseline/2) ($baseline*1.5) ($baseline/2); diff --git a/lms/static/sass/views/_teams.scss b/lms/static/sass/views/_teams.scss index 53140ecd7d..7f1c47d827 100644 --- a/lms/static/sass/views/_teams.scss +++ b/lms/static/sass/views/_teams.scss @@ -49,16 +49,17 @@ .page-header.has-secondary { - .page-header-main { - display: inline-block; - width: flex-grid(8,12); - } + .page-header-main { + display: inline-block; + width: flex-grid(8,12); + } - .page-header-secondary { - display: inline-block; - width: flex-grid(4,12); - @include text-align(right); - } + .page-header-secondary { + @include text-align(right); + display: inline-block; + width: flex-grid(4,12); + vertical-align: text-bottom; + } } // ui bits @@ -83,41 +84,56 @@ .page-header-search { + .wrapper-search-input { + display: inline-block; + position: relative; + vertical-align: middle; + } + .search-label { @extend %text-sr; } .search-field { transition: all $tmg-f2 ease-in-out; - border: 0; - border-bottom: 2px solid transparent; + border: 1px solid $gray-l4; + border-radius: 3px; padding: ($baseline/4) ($baseline/2); font-family: inherit; color: $gray; - @include text-align(right); - - &:focus { - border-bottom: 2px solid $black; - color: $black; - } } .action-search { @extend %button-reset; - padding: ($baseline/4) ($baseline/2); - + background-color: $gray-l3; + padding: ($baseline/5) ($baseline/2); + text-shadow: none; + vertical-align: middle; .icon { - color: $gray-l3; + color: $white; } // STATE: hover and focus &:hover, &:focus { + background-color: $blue; + } + } - .icon { - color: $black; - } + .action-clear { + @include right(0); + @include margin(0, ($baseline/4), 0, 0); + @extend %button-reset; + position: absolute; + top: 0; + padding: ($baseline/4); + color: $gray-l3; + + // STATE: hover and focus + &:hover, + &:focus { + color: $black; } } } @@ -164,6 +180,7 @@ label { // override color: inherit; font-size: inherit; + cursor: auto; } .listing-sort-select { @@ -247,28 +264,40 @@ color: $gray; .meta-detail { - margin-top: ($baseline/4); - @include margin-right ($baseline*.75); + @include margin(($baseline/4) ($baseline*.75) ($baseline/4) 0); color: $gray-d1; + abbr { + border: 0; + text-decoration: none; + } + .icon { @include margin-right ($baseline/4); } } + .member-count { + display: inline-block; + vertical-align: middle; + @include margin-right($baseline/4); + } + .list-member-thumbs { @extend %ui-no-list; display: inline-block; - vertical-align: text-bottom; + vertical-align: middle; .item-member-thumb { display: inline-block; + margin: 0 ($baseline/4); } img { width: $baseline; height: $baseline; - margin: 0 ($baseline/4); + border-radius: ($baseline/5); + border: 1px solid $gray-l4; } } } @@ -309,6 +338,7 @@ .meta-detail { display: inline-block; + vertical-align: middle; } .team-activity { @@ -324,10 +354,6 @@ &.has-pennant { - .wrapper-card-core { - padding-top: ($baseline*2); - } - .pennant { @extend %t-copy-sub2; @extend %t-strong; @@ -578,19 +604,24 @@ .teams-main { .team-edit-fields { + overflow: hidden; + position: relative; + width: 100%; @include clearfix(); .team-required-fields { @include float(left); width: 55%; - border-right: 2px solid $gray-l4;; + + .u-field { + @include margin-right($baseline*2); + } .u-field.u-field-name { padding-bottom: $baseline; .u-field-value { - display: block; - width: 90%; + width: 100%; input { border-radius: ($baseline/5); @@ -608,36 +639,38 @@ .u-field.u-field-description { .u-field-value { - display: block; width: 100%; textarea { + width: 100%; height: ($baseline*5); - width: 90%; border-radius: ($baseline/5) } } - .u-field-message { - display: block; - @extend %t-copy-sub1; - @include padding-left(0); - margin-top: ($baseline/4); - color: $gray-l1; - width: 90%; + .u-field-footer { + + .u-field-message { + display: block; + @extend %t-copy-sub1; + @include padding-left(0); + margin-top: ($baseline/4); + color: $gray-l1; + width: 100%; + } } } .u-field-title { padding-bottom: ($baseline/4); color: $base-font-color; - width: 40%; + width: 100%; } } .team-optional-fields { @include float(left); - @include margin-left($baseline); + @include margin-left($baseline*2); width: 40%; .u-field.u-field-optional_description { @@ -647,7 +680,6 @@ color: $base-font-color; font-weight: $font-semibold; margin-bottom: ($baseline/5); - width: 100%; } .u-field-value { @@ -663,19 +695,16 @@ display: none; } - .u-field-value { - width: 90%; - } - .u-field-title { - display: block; color: $base-font-color; - width: 35%; } .u-field-message { @include padding-left(0); - width: 95%; + } + + .u-field-title, .u-field-value, .u-field-message { + width: 100%; } } @@ -701,6 +730,14 @@ } } +// vertical line between required and optional fields +.vertical-line:after { + height: 100%; + border-left: 2px solid $gray-l4; + content: ""; + position: absolute; +} + .form-instructions { margin: ($baseline/2) 0 $baseline 0; } diff --git a/lms/templates/components/header/header.underscore b/lms/templates/components/header/header.underscore index 5fd91299da..4ab011b38f 100644 --- a/lms/templates/components/header/header.underscore +++ b/lms/templates/components/header/header.underscore @@ -12,5 +12,5 @@ <h2 class="page-title"><%- title %></h2> <p class="page-description"><%- description %></p> </div> - <div class="header-action-view"></div> + <div class="page-header-secondary"></div> </header> diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index 943a92cc51..dc4a5d8994 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -10,7 +10,7 @@ from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> </%def> -<%block name="bodyclass">view-incourse view-courseware courseware ${course.css_class or ''}</%block> +<%block name="bodyclass">view-in-course view-courseware courseware ${course.css_class or ''}</%block> <%block name="title"><title> % if section_title: ${page_title_breadcrumbs(section_title, course_name())} diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 224a4efcf7..08676e583d 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -14,7 +14,7 @@ from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> </%def> -<%block name="bodyclass">view-incourse view-courseware courseware ${course.css_class or ''}</%block> +<%block name="bodyclass">view-in-course view-courseware courseware ${course.css_class or ''}</%block> <%block name="title"><title> % if section_title: ${page_title_breadcrumbs(section_title, course_name())} diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html index 346332819b..fb19cc1339 100644 --- a/lms/templates/courseware/info.html +++ b/lms/templates/courseware/info.html @@ -42,7 +42,7 @@ $(document).ready(function(){ </script> </%block> -<%block name="bodyclass">view-incourse view-course-info ${course.css_class or ''}</%block> +<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block> <section class="container"> <div class="info-wrapper"> % if user.is_authenticated(): diff --git a/lms/templates/courseware/legacy_instructor_dashboard.html b/lms/templates/courseware/legacy_instructor_dashboard.html index 2aa0376a9e..28724da62d 100644 --- a/lms/templates/courseware/legacy_instructor_dashboard.html +++ b/lms/templates/courseware/legacy_instructor_dashboard.html @@ -361,9 +361,8 @@ function goto( mode) %if modeflag.get('Data'): <hr width="40%" style="align:left"> - <p> ${_("Problem urlname:")} - <input type="text" name="problem_to_dump" size="40"> - <input type="submit" name="action" value="Download CSV of all responses to problem"> + <p class="is-deprecated"> + ${_("To download a CSV listing student responses to a given problem, visit the Data Download section of the Instructor Dashboard.")} </p> <p class="is-deprecated"> diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 564ded6c8c..b1fe59fd91 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -7,7 +7,7 @@ from util.date_utils import get_time_display, DEFAULT_SHORT_DATE_FORMAT from django.conf import settings from django.utils.http import urlquote_plus %> -<%block name="bodyclass">view-incourse view-progress</%block> +<%block name="bodyclass">view-in-course view-progress</%block> <%block name="headextra"> <%static:css group='style-course-vendor'/> diff --git a/lms/templates/courseware/static_tab.html b/lms/templates/courseware/static_tab.html index b080dfda85..bf199c398f 100644 --- a/lms/templates/courseware/static_tab.html +++ b/lms/templates/courseware/static_tab.html @@ -1,5 +1,5 @@ <%inherit file="/main.html" /> -<%block name="bodyclass">view-incourse view-statictab ${course.css_class or ''}</%block> +<%block name="bodyclass">view-in-course view-statictab ${course.css_class or ''}</%block> <%namespace name='static' file='/static_content.html'/> <%block name="headextra"> diff --git a/lms/templates/emails/enroll_email_allowedmessage.txt b/lms/templates/emails/enroll_email_allowedmessage.txt index 49ad62d8ab..8a24dedde0 100644 --- a/lms/templates/emails/enroll_email_allowedmessage.txt +++ b/lms/templates/emails/enroll_email_allowedmessage.txt @@ -4,7 +4,7 @@ ${_("Dear student,")} ${_("You have been invited to join {course_name} at {site_name} by a " "member of the course staff.").format( - course_name=course.display_name_with_default, + course_name=display_name or course.display_name_with_default, site_name=site_name )} % if is_shib_course: @@ -26,13 +26,13 @@ ${_("To finish your registration, please visit {registration_url} and fill " % if auto_enroll: ${_("Once you have registered and activated your account, you will see " "{course_name} listed on your dashboard.").format( - course_name=course.display_name_with_default + course_name=display_name or course.display_name_with_default )} % elif course_about_url is not None: ${_("Once you have registered and activated your account, visit {course_about_url} " "to join the course.").format(course_about_url=course_about_url)} % else: -${_("You can then enroll in {course_name}.").format(course_name=course.display_name_with_default)} +${_("You can then enroll in {course_name}.").format(course_name=display_name or course.display_name_with_default)} % endif % endif diff --git a/lms/templates/emails/enroll_email_allowedsubject.txt b/lms/templates/emails/enroll_email_allowedsubject.txt index 6ed7ce61b5..186b84b9ce 100644 --- a/lms/templates/emails/enroll_email_allowedsubject.txt +++ b/lms/templates/emails/enroll_email_allowedsubject.txt @@ -1,5 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> ${_("You have been invited to register for {course_name}").format( - course_name=course.display_name_with_default + course_name=display_name or course.display_name_with_default )} \ No newline at end of file diff --git a/lms/templates/emails/unenroll_email_allowedmessage.txt b/lms/templates/emails/unenroll_email_allowedmessage.txt index 6e3386738e..f42786ff49 100644 --- a/lms/templates/emails/unenroll_email_allowedmessage.txt +++ b/lms/templates/emails/unenroll_email_allowedmessage.txt @@ -4,7 +4,7 @@ ${_("Dear Student,")} ${_("You have been un-enrolled from course {course_name} by a member " "of the course staff. Please disregard the invitation " - "previously sent.").format(course_name=course.display_name_with_default)} + "previously sent.").format(course_name=display_name or course.display_name_with_default)} ---- ${_("This email was automatically sent from {site_name} " diff --git a/lms/templates/emails/unenroll_email_enrolledmessage.txt b/lms/templates/emails/unenroll_email_enrolledmessage.txt index 9a6e5d9161..a74e167758 100644 --- a/lms/templates/emails/unenroll_email_enrolledmessage.txt +++ b/lms/templates/emails/unenroll_email_enrolledmessage.txt @@ -5,7 +5,7 @@ ${_("Dear {full_name}").format(full_name=full_name)} ${_("You have been un-enrolled in {course_name} at {site_name} by a member " "of the course staff. The course will no longer appear on your " "{site_name} dashboard.").format( - course_name=course.display_name_with_default, site_name=site_name + course_name=display_name or course.display_name_with_default, site_name=site_name )} ${_("Your other courses have not been affected.")} diff --git a/lms/templates/emails/unenroll_email_subject.txt b/lms/templates/emails/unenroll_email_subject.txt index 9dd348e2b6..65028ff7fe 100644 --- a/lms/templates/emails/unenroll_email_subject.txt +++ b/lms/templates/emails/unenroll_email_subject.txt @@ -1,5 +1,5 @@ <%! from django.utils.translation import ugettext as _ %> ${_("You have been un-enrolled from {course_name}").format( - course_name=course.display_name_with_default + course_name=display_name or course.display_name_with_default )} \ No newline at end of file diff --git a/lms/templates/header_extra.html b/lms/templates/header_extra.html new file mode 100644 index 0000000000..4a2ea9dda3 --- /dev/null +++ b/lms/templates/header_extra.html @@ -0,0 +1,3 @@ +<% +# This template is left blank on purpose. It can be overridden by microsites. +%> diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html index a38ba21aac..36f897be16 100644 --- a/lms/templates/instructor/instructor_dashboard_2/data_download.html +++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -39,6 +39,19 @@ <p>${_("Click to generate a CSV file of all proctored exam results in this course.")}</p> <p><input type="button" name="proctored-exam-results-report" value="${_("Generate Proctored Exam Results Report")}" data-endpoint="${ section_data['list_proctored_results_url'] }"/></p> %endif + + <p>${_("To generate a CSV file that lists all student answers to a given problem, enter the location of the problem (from its Staff Debug Info).")}</p> + + <p> + <label> + <span>${_("Problem location: ")}</span> + <input type="text" name="problem-location" /> + </label> + </p> + <p> + <input type="button" name="list-problem-responses-csv" value="${_("Download a CSV of problem responses")}" data-endpoint="${ section_data['get_problem_responses_url'] }" data-csv="true"> + </p> + % if not disable_buttons: <p>${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}</p> <p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"></p> @@ -54,7 +67,7 @@ %endif <div class="request-response msg msg-confirm copy" id="report-request-response"></div> - <div class="request-response-error msg msg-warning copy" id="report-request-response-error"></div> + <div class="request-response-error msg msg-error copy" id="report-request-response-error"></div> <br> <p><b>${_("Reports Available for Download")}</b></p> diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index 5a0daee732..b763e85fb6 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -223,7 +223,7 @@ import pytz collapsible: true }); - $('a[rel*=leanModal]').leanModal(); + $('a[rel*=leanModal]').leanModal({ top : -70, position: "absolute", closeButton: ".modal_close" }); $.each($("a.edit-right"), function () { if ($(this).parent().parent('tr').hasClass('inactive_coupon')) { $(this).removeAttr('href') 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 4d38718253..5b47c98c12 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse %> -<%block name="bodyclass">view-incourse view-instructordash</%block> +<%block name="bodyclass">view-in-course view-instructordash</%block> ## ----- Tips on adding something to the new instructor dashboard ----- ## 1. add your input element, e.g. in instructor_dashboard2/data_download.html diff --git a/lms/templates/login.html b/lms/templates/login.html index c6483df2a0..b0012d1d01 100644 --- a/lms/templates/login.html +++ b/lms/templates/login.html @@ -219,7 +219,7 @@ from microsite_configuration import microsite <div class="form-actions form-third-party-auth"> - % for enabled in provider.Registry.enabled(): + % for enabled in provider.Registry.accepting_logins(): ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn). <button type="submit" class="button button-primary button-${enabled.provider_id} login-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.provider_id]}');"><span class="icon fa ${enabled.icon_class}"></span>${_('Sign in with {provider_name}').format(provider_name=enabled.name)}</button> % endfor diff --git a/lms/templates/main.html b/lms/templates/main.html index 40c62ee6f3..0f9839ace7 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -94,7 +94,7 @@ from branding import api as branding_api style_overrides_file = None else: - header_extra_file = None + header_extra_file = microsite.get_template_path('header_extra.html') if settings.FEATURES['IS_EDX_DOMAIN'] and not is_microsite(): header_file = microsite.get_template_path('navigation-edx.html') diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 809f4b07e8..48154a5956 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -97,7 +97,7 @@ site_status_msg = get_site_status_msg(course_id) </ul> </li> </ol> - % if should_display_shopping_cart_func(): # see shoppingcart.context_processor.user_has_cart_context_processor + % if should_display_shopping_cart_func() and not (course and microsite.is_request_in_microsite()): # see shoppingcart.context_processor.user_has_cart_context_processor <ol class="user"> <li class="primary"> <a class="shopping-cart" href="${reverse('shoppingcart.views.show_cart')}"> diff --git a/lms/templates/register.html b/lms/templates/register.html index c913be8466..46fc6c7d47 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -130,7 +130,7 @@ import calendar <div class="form-actions form-third-party-auth"> - % for enabled in provider.Registry.enabled(): + % for enabled in provider.Registry.accepting_logins(): ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). <button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');"><span class="icon fa ${enabled.icon_class}"></span>${_('Sign up with {provider_name}').format(provider_name=enabled.name)}</button> % endfor diff --git a/lms/templates/student_profile/third_party_auth.html b/lms/templates/student_profile/third_party_auth.html index 6092ee7034..9eea38ddda 100644 --- a/lms/templates/student_profile/third_party_auth.html +++ b/lms/templates/student_profile/third_party_auth.html @@ -32,7 +32,7 @@ from third_party_auth import pipeline ## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn). ${_("Unlink")} </a> - % else: + % elif state.provider.accepts_logins: <a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}"> ## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn). ${_("Link")} diff --git a/lms/templates/survey/survey.html b/lms/templates/survey/survey.html index c71b945178..f0b8dc1eb9 100644 --- a/lms/templates/survey/survey.html +++ b/lms/templates/survey/survey.html @@ -21,6 +21,8 @@ from django.utils import html <input type="hidden" name="_redirect_url" value="${redirect_url}" /> % if course: + <input type="hidden" name="course_id" value="${unicode(course.id)}" /> + <div class="header-survey"> <h4 class="course-info"> <span class="course-org">${course.display_org_with_default}</span><span class="course-number"> ${course.display_number_with_default}</span> diff --git a/lms/templates/wiki/base.html b/lms/templates/wiki/base.html index 9493b92868..7191443c32 100644 --- a/lms/templates/wiki/base.html +++ b/lms/templates/wiki/base.html @@ -3,7 +3,7 @@ {% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}{% endblock %} -{% block bodyclass %}view-incourse view-wiki{% endblock %} +{% block bodyclass %}view-in-course view-wiki{% endblock %} {% block headextra %} diff --git a/openedx/core/djangoapps/credit/api/eligibility.py b/openedx/core/djangoapps/credit/api/eligibility.py index 91aa3f79e4..758617bad5 100644 --- a/openedx/core/djangoapps/credit/api/eligibility.py +++ b/openedx/core/djangoapps/credit/api/eligibility.py @@ -284,6 +284,59 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, log.error("Error sending email") +# pylint: disable=invalid-name +def remove_credit_requirement_status(username, course_key, req_namespace, req_name): + """ + Remove the user's requirement status. + + This will remove the record from the credit requirement status table. + The user will still be eligible for the credit in a course. + + Args: + username (str): Username of the user + course_key (CourseKey): Identifier for the course associated + with the requirement. + req_namespace (str): Namespace of the requirement + (e.g. "grade" or "reverification") + req_name (str): Name of the requirement + (e.g. "grade" or the location of the ICRV XBlock) + + Example: + >>> remove_credit_requirement_status( + "staff", + CourseKey.from_string("course-v1-edX-DemoX-1T2015"), + "reverification", + "i4x://edX/DemoX/edx-reverification-block/assessment_uuid". + ) + + """ + + # Find the requirement we're trying to remove + req_to_remove = CreditRequirement.get_course_requirements(course_key, namespace=req_namespace, name=req_name) + + # If we can't find the requirement, then the most likely explanation + # is that there was a lag removing the credit requirements after the course + # was published. We *could* attempt to remove the requirement here, + # but that could cause serious performance issues if many users attempt to + # lock the row at the same time. + # Instead, we skip removing the requirement and log an error. + if req_to_remove is None: + log.error( + ( + u'Could not remove credit requirement in course "%s" ' + u'with namespace "%s" and name "%s" ' + u'because the requirement does not exist. ' + ), + unicode(course_key), req_namespace, req_name + ) + return + + # Remove the requirement status + CreditRequirementStatus.remove_requirement_status( + username, req_to_remove + ) + + def get_credit_requirement_status(course_key, username, namespace=None, name=None): """ Retrieve the user's status for each credit requirement in the course. diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index 45d2ab812f..b77505daec 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -467,6 +467,24 @@ class CreditRequirementStatus(TimeStampedModel): requirement_status.reason = reason if reason else {} requirement_status.save() + @classmethod + @transaction.commit_on_success + def remove_requirement_status(cls, username, requirement): + """ + Remove credit requirement status for given username. + + Args: + username(str): Username of the user + requirement(CreditRequirement): 'CreditRequirement' object + """ + + try: + requirement_status = cls.objects.get(username=username, requirement=requirement) + requirement_status.delete() + except cls.DoesNotExist: + log.exception(u'The requirement status does not exist against the username %s.', username) + return + class CreditEligibility(TimeStampedModel): """ diff --git a/openedx/core/djangoapps/credit/services.py b/openedx/core/djangoapps/credit/services.py index 67e80d1610..423f3e4b25 100644 --- a/openedx/core/djangoapps/credit/services.py +++ b/openedx/core/djangoapps/credit/services.py @@ -159,3 +159,54 @@ class CreditService(object): status, reason ) + + def remove_credit_requirement_status(self, user_id, course_key_or_id, req_namespace, req_name): + """ + A simple wrapper around the method of the same name in + api.eligibility.py. The only difference is that a user_id + is passed in. + + For more information, see documentation on this method name + in api.eligibility.py + """ + + # This seems to need to be here otherwise we get + # circular references when starting up the app + from openedx.core.djangoapps.credit.api.eligibility import ( + is_credit_course, + remove_credit_requirement_status as api_remove_credit_requirement_status + ) + + course_key = _get_course_key(course_key_or_id) + + # quick exit, if course is not credit enabled + if not is_credit_course(course_key): + return + + # always log any deleted activity to the credit requirements + # table. This will be to help debug any issues that might + # arise in production + log_msg = ( + 'remove_credit_requirement_status was called with ' + 'user_id={user_id}, course_key_or_id={course_key_or_id} ' + 'req_namespace={req_namespace}, req_name={req_name}, '.format( + user_id=user_id, + course_key_or_id=course_key_or_id, + req_namespace=req_namespace, + req_name=req_name + ) + ) + log.info(log_msg) + + # need to get user_name from the user object + try: + user = User.objects.get(id=user_id) + except ObjectDoesNotExist: + return None + + api_remove_credit_requirement_status( + user.username, + course_key, + req_namespace, + req_name + ) diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index b86b74fff2..2a5ee8af1c 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -318,6 +318,63 @@ class CreditRequirementApiTests(CreditApiTestBase): req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], "failed") + def test_remove_credit_requirement_status(self): + self.add_credit_course() + requirements = [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + }, + }, + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "display_name": "Assessment 1", + "criteria": {}, + } + ] + + api.set_credit_requirements(self.course_key, requirements) + course_requirements = api.get_credit_requirements(self.course_key) + self.assertEqual(len(course_requirements), 2) + + # before setting credit_requirement_status + api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + self.assertIsNone(req_status[0]["status"]) + self.assertIsNone(req_status[0]["status_date"]) + self.assertIsNone(req_status[0]["reason"]) + + # Set the requirement to "satisfied" and check that it's actually set + api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + self.assertEqual(len(req_status), 1) + self.assertEqual(req_status[0]["status"], "satisfied") + + # remove the credit requirement status and check that it's actually removed + api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + self.assertIsNone(req_status[0]["status"]) + self.assertIsNone(req_status[0]["status_date"]) + self.assertIsNone(req_status[0]["reason"]) + + def test_remove_credit_requirement_status_req_not_configured(self): + # Configure a credit course with no requirements + self.add_credit_course() + + # A user satisfies a requirement. This could potentially + # happen if there's a lag when the requirements are removed + # after the course is published. + api.remove_credit_requirement_status("bob", self.course_key, "grade", "grade") + + # Since the requirement hasn't been published yet, it won't show + # up in the list of requirements. + req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") + self.assertEqual(len(req_status), 0) + def test_satisfy_all_requirements(self): """ Test the credit requirements, eligibility notification, email content caching for a credit course. diff --git a/openedx/core/djangoapps/credit/tests/test_services.py b/openedx/core/djangoapps/credit/tests/test_services.py index 8cc471eba5..c250ce0074 100644 --- a/openedx/core/djangoapps/credit/tests/test_services.py +++ b/openedx/core/djangoapps/credit/tests/test_services.py @@ -121,6 +121,117 @@ class CreditServiceTests(ModuleStoreTestCase): self.assertEqual(credit_state['credit_requirement_status'][0]['name'], 'grade') self.assertEqual(credit_state['credit_requirement_status'][0]['status'], 'satisfied') + def test_remove_credit_requirement_status(self): + """ + Happy path when deleting the requirement status. + """ + self.assertTrue(self.service.is_credit_course(self.course.id)) + + CourseEnrollment.enroll(self.user, self.course.id) + + # set course requirements + set_credit_requirements( + self.course.id, + [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + }, + }, + ] + ) + + # mark the grade as satisfied + self.service.set_credit_requirement_status( + self.user.id, + self.course.id, + 'grade', + 'grade' + ) + + # now the status should be "satisfied" when looking at the credit_requirement_status list + credit_state = self.service.get_credit_state(self.user.id, self.course.id) + self.assertEqual(credit_state['credit_requirement_status'][0]['status'], "satisfied") + + # remove the requirement status. + self.service.remove_credit_requirement_status( + self.user.id, + self.course.id, + 'grade', + 'grade' + ) + + # now the status should be None when looking at the credit_requirement_status list + credit_state = self.service.get_credit_state(self.user.id, self.course.id) + self.assertEqual(credit_state['credit_requirement_status'][0]['status'], None) + + def test_invalid_user(self): + """ + Try removing requirement status with a invalid user_id + """ + + # set course requirements + set_credit_requirements( + self.course.id, + [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + }, + }, + ] + ) + + # mark the grade as satisfied + retval = self.service.set_credit_requirement_status( + self.user.id, + self.course.id, + 'grade', + 'grade' + ) + self.assertIsNone(retval) + + # remove the requirement status with the invalid user id + retval = self.service.remove_credit_requirement_status( + 0, + self.course.id, + 'grade', + 'grade' + ) + self.assertIsNone(retval) + + def test_remove_status_non_credit(self): + """ + assert that we can still try to update a credit status but return quickly if + a course is not credit eligible + """ + + no_credit_course = CourseFactory.create(org='NoCredit', number='NoCredit', display_name='Demo_Course') + + self.assertFalse(self.service.is_credit_course(no_credit_course.id)) + + CourseEnrollment.enroll(self.user, no_credit_course.id) + + # this should be a no-op + self.service.remove_credit_requirement_status( + self.user.id, + no_credit_course.id, + 'grade', + 'grade' + ) + + credit_state = self.service.get_credit_state(self.user.id, no_credit_course.id) + + self.assertIsNotNone(credit_state) + self.assertFalse(credit_state['is_credit_course']) + self.assertEqual(len(credit_state['credit_requirement_status']), 0) + def test_course_name(self): """ Make sure we can get back the optional course name diff --git a/openedx/core/djangoapps/credit/tests/test_views.py b/openedx/core/djangoapps/credit/tests/test_views.py index 48118e4e58..24ac8ea983 100644 --- a/openedx/core/djangoapps/credit/tests/test_views.py +++ b/openedx/core/djangoapps/credit/tests/test_views.py @@ -8,7 +8,7 @@ import unittest import ddt from django.conf import settings from django.core.urlresolvers import reverse -from django.test import TestCase +from django.test import TestCase, Client from django.test.utils import override_settings from mock import patch from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory @@ -380,6 +380,34 @@ class CreditCourseViewSetTests(TestCase): response = self.client.get(self.path) self.assertEqual(response.status_code, 200) + def test_session_auth_post_requires_csrf_token(self): + """ Verify non-GET requests require a CSRF token be attached to the request. """ + user = UserFactory(password=self.password, is_staff=True) + client = Client(enforce_csrf_checks=True) + self.assertTrue(client.login(username=user.username, password=self.password)) + + data = { + 'course_key': 'a/b/c', + 'enabled': True + } + + # POSTs without a CSRF token should fail. + response = client.post(self.path, data=json.dumps(data), content_type=JSON) + + # NOTE (CCB): Ordinarily we would expect a 403; however, since the CSRF validation and session authentication + # fail, DRF considers the request to be unauthenticated. + self.assertEqual(response.status_code, 401) + self.assertIn('CSRF', response.content) + + # Retrieve a CSRF token + response = client.get('/dashboard') + csrf_token = response.cookies[settings.CSRF_COOKIE_NAME].value # pylint: disable=no-member + self.assertGreater(len(csrf_token), 0) + + # Ensure POSTs made with the token succeed. + response = client.post(self.path, data=json.dumps(data), content_type=JSON, HTTP_X_CSRFTOKEN=csrf_token) + self.assertEqual(response.status_code, 201) + def test_oauth(self): """ Verify the endpoint supports OAuth, and only allows authorization for staff users. """ user = UserFactory(is_staff=False) diff --git a/openedx/core/djangoapps/credit/views.py b/openedx/core/djangoapps/credit/views.py index 22a05b6e12..9ebf90b361 100644 --- a/openedx/core/djangoapps/credit/views.py +++ b/openedx/core/djangoapps/credit/views.py @@ -12,6 +12,7 @@ from django.http import ( HttpResponseForbidden, Http404 ) +from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST, require_GET from opaque_keys import InvalidKeyError @@ -379,6 +380,9 @@ class CreditCourseViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, view authentication_classes = (authentication.OAuth2Authentication, authentication.SessionAuthentication,) permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser) + # This CSRF exemption only applies when authenticating without SessionAuthentication. + # SessionAuthentication will enforce CSRF protection. + @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): # Convert the course ID/key from a string to an actual CourseKey object. course_id = kwargs.get(self.lookup_field, None) diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py index 34efebcccd..042d5870c0 100644 --- a/openedx/core/djangoapps/user_api/views.py +++ b/openedx/core/djangoapps/user_api/views.py @@ -722,27 +722,28 @@ class RegistrationView(APIView): if running_pipeline: current_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) - # Override username / email / full name - field_overrides = current_provider.get_register_form_data( - running_pipeline.get('kwargs') - ) + if current_provider: + # Override username / email / full name + field_overrides = current_provider.get_register_form_data( + running_pipeline.get('kwargs') + ) - for field_name in self.DEFAULT_FIELDS: - if field_name in field_overrides: - form_desc.override_field_properties( - field_name, default=field_overrides[field_name] - ) + for field_name in self.DEFAULT_FIELDS: + if field_name in field_overrides: + form_desc.override_field_properties( + field_name, default=field_overrides[field_name] + ) - # Hide the password field - form_desc.override_field_properties( - "password", - default="", - field_type="hidden", - required=False, - label="", - instructions="", - restrictions={} - ) + # Hide the password field + form_desc.override_field_properties( + "password", + default="", + field_type="hidden", + required=False, + label="", + instructions="", + restrictions={} + ) class PasswordResetView(APIView): diff --git a/openedx/tests/__init__.py b/openedx/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/tests/xblock_integration/__init__.py b/openedx/tests/xblock_integration/__init__.py new file mode 100644 index 0000000000..66adf2540e --- /dev/null +++ b/openedx/tests/xblock_integration/__init__.py @@ -0,0 +1,7 @@ +"""Tests of XBlocks integrated with edx-platform. + +These tests exercise XBlocks which may live in other repos, to confirm both +that the XBlock works, and that edx-platform continues to properly support the +XBlock. + +""" diff --git a/lms/djangoapps/courseware/tests/test_recommender.py b/openedx/tests/xblock_integration/test_recommender.py similarity index 98% rename from lms/djangoapps/courseware/tests/test_recommender.py rename to openedx/tests/xblock_integration/test_recommender.py index b16be8cd01..dea42322d6 100644 --- a/lms/djangoapps/courseware/tests/test_recommender.py +++ b/openedx/tests/xblock_integration/test_recommender.py @@ -2,21 +2,24 @@ This test file will run through some XBlock test scenarios regarding the recommender system """ + +from copy import deepcopy import json import itertools import StringIO +import unittest + from ddt import ddt, data -from copy import deepcopy from nose.plugins.attrib import attr +from django.conf import settings from django.core.urlresolvers import reverse from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from courseware.tests.helpers import LoginEnrollmentTestCase -from courseware.tests.factories import GlobalStaffFactory - +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase +from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory from lms.djangoapps.lms_xblock.runtime import quote_slashes @@ -32,6 +35,12 @@ class TestRecommender(SharedModuleStoreTestCase, LoginEnrollmentTestCase): @classmethod def setUpClass(cls): + # Nose runs setUpClass methods even if a class decorator says to skip + # the class: https://github.com/nose-devs/nose/issues/946 + # So, skip the test class here if we are not in the LMS. + if settings.ROOT_URLCONF != 'lms.urls': + raise unittest.SkipTest('Test only valid in lms') + super(TestRecommender, cls).setUpClass() cls.course = CourseFactory.create( display_name='Recommender_Test_Course' diff --git a/pavelib/assets.py b/pavelib/assets.py index c159986c35..be50c9eee0 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -132,7 +132,10 @@ def compile_coffeescript(*files): @task @no_help -@cmdopts([('debug', 'd', 'Debug mode')]) +@cmdopts([ + ('debug', 'd', 'Debug mode'), + ('force', '', 'Force full compilation'), +]) def compile_sass(options): """ Compile Sass to CSS. @@ -146,6 +149,9 @@ def compile_sass(options): parts.append("--sourcemap") else: parts.append("--style compressed --quiet") + if options.get('force'): + parts.append("--force") + parts.append("--load-path .") for load_path in SASS_LOAD_PATHS + SASS_DIRS.keys(): parts.append("--load-path {path}".format(path=load_path)) diff --git a/pavelib/paver_tests/test_paver_quality.py b/pavelib/paver_tests/test_paver_quality.py index 81dd096046..db08061eee 100644 --- a/pavelib/paver_tests/test_paver_quality.py +++ b/pavelib/paver_tests/test_paver_quality.py @@ -187,7 +187,7 @@ class TestPaverRunQuality(unittest.TestCase): @patch('__builtin__.open', mock_open()) def test_failure_on_diffquality_pep8(self): """ - If pep8 finds errors, pylint should still be run + If pep8 finds errors, pylint and jshint should still be run """ # Mock _get_pep8_violations to return a violation _mock_pep8_violations = MagicMock( @@ -198,10 +198,10 @@ class TestPaverRunQuality(unittest.TestCase): pavelib.quality.run_quality("") self.assertRaises(BuildFailure) - # Test that both pep8 and pylint were called by counting the calls to _get_pep8_violations - # (for pep8) and sh (for diff-quality pylint) + # Test that pep8, pylint, and jshint were called by counting the calls to + # _get_pep8_violations (for pep8) and sh (for diff-quality pylint & jshint) self.assertEqual(_mock_pep8_violations.call_count, 1) - self.assertEqual(self._mock_paver_sh.call_count, 1) + self.assertEqual(self._mock_paver_sh.call_count, 2) @patch('__builtin__.open', mock_open()) def test_failure_on_diffquality_pylint(self): @@ -219,8 +219,28 @@ class TestPaverRunQuality(unittest.TestCase): # Test that both pep8 and pylint were called by counting the calls # Assert that _get_pep8_violations (which calls "pep8") is called once self.assertEqual(_mock_pep8_violations.call_count, 1) - # And assert that sh was called once (for the call to "pylint") - self.assertEqual(self._mock_paver_sh.call_count, 1) + # And assert that sh was called twice (for the calls to pylint & jshint). This means that even in + # the event of a diff-quality pylint failure, jshint is still called. + self.assertEqual(self._mock_paver_sh.call_count, 2) + + @patch('__builtin__.open', mock_open()) + def test_failure_on_diffquality_jshint(self): + """ + If diff-quality fails on jshint, the paver task should also fail + """ + + # Underlying sh call must fail when it is running the jshint diff-quality task + self._mock_paver_sh.side_effect = CustomShMock().fail_on_jshint + _mock_pep8_violations = MagicMock(return_value=(0, [])) + with patch('pavelib.quality._get_pep8_violations', _mock_pep8_violations): + with self.assertRaises(SystemExit): + pavelib.quality.run_quality("") + self.assertRaises(BuildFailure) + # Test that both pep8 and pylint were called by counting the calls + # Assert that _get_pep8_violations (which calls "pep8") is called once + self.assertEqual(_mock_pep8_violations.call_count, 1) + # And assert that sh was called twice (for the calls to pep8 and pylint) + self.assertEqual(self._mock_paver_sh.call_count, 2) @patch('__builtin__.open', mock_open()) def test_other_exception(self): @@ -243,8 +263,8 @@ class TestPaverRunQuality(unittest.TestCase): pavelib.quality.run_quality("") # Assert that _get_pep8_violations (which calls "pep8") is called once self.assertEqual(_mock_pep8_violations.call_count, 1) - # And assert that sh was called once (for the call to "pylint") - self.assertEqual(self._mock_paver_sh.call_count, 1) + # And assert that sh was called twice (for the call to "pylint" & "jshint") + self.assertEqual(self._mock_paver_sh.call_count, 2) class CustomShMock(object): @@ -263,3 +283,14 @@ class CustomShMock(object): paver.easy.sh("exit 1") else: return + + def fail_on_jshint(self, arg): + """ + For our tests, we need the call for diff-quality running pep8 reports to fail, since that is what + is going to fail when we pass in a percentage ("p") requirement. + """ + if "jshint" in arg: + # Essentially mock diff-quality exiting with 1 + paver.easy.sh("exit 1") + else: + return diff --git a/pavelib/paver_tests/test_servers.py b/pavelib/paver_tests/test_servers.py index 6c3267dd23..3dbf20388e 100644 --- a/pavelib/paver_tests/test_servers.py +++ b/pavelib/paver_tests/test_servers.py @@ -12,7 +12,7 @@ EXPECTED_COFFEE_COMMAND = ( ) EXPECTED_SASS_COMMAND = ( "sass --update --cache-location /tmp/sass-cache --default-encoding utf-8 --style compressed" - " --quiet --load-path common/static --load-path common/static/sass" + " --quiet --load-path . --load-path common/static --load-path common/static/sass" " --load-path lms/static/sass --load-path lms/static/certificates/sass" " --load-path cms/static/sass --load-path common/static/sass" " lms/static/sass:lms/static/css lms/static/certificates/sass:lms/static/certificates/css" @@ -30,6 +30,9 @@ EXPECTED_CELERY_COMMAND = ( EXPECTED_RUN_SERVER_COMMAND = ( "python manage.py {system} --settings={settings} runserver --traceback --pythonpath=. 0.0.0.0:{port}" ) +EXPECTED_INDEX_COURSE_COMMAND = ( + "python manage.py {system} --settings={settings} reindex_course --setup" +) @ddt.ddt @@ -83,13 +86,27 @@ class TestPaverServerTasks(PaverTestCase): Test the "devstack" task. """ options = server_options.copy() + is_optimized = options.get("optimized", False) + expected_settings = "devstack_optimized" if is_optimized else options.get("settings", "devstack") # First test with LMS options["system"] = "lms" + options["expected_messages"] = [ + EXPECTED_INDEX_COURSE_COMMAND.format( + system="cms", + settings=expected_settings, + ) + ] self.verify_server_task("devstack", options, contracts_default=True) # Then test with Studio options["system"] = "cms" + options["expected_messages"] = [ + EXPECTED_INDEX_COURSE_COMMAND.format( + system="cms", + settings=expected_settings, + ) + ] self.verify_server_task("devstack", options, contracts_default=True) @ddt.data( @@ -196,7 +213,7 @@ class TestPaverServerTasks(PaverTestCase): call_task("pavelib.servers.devstack", args=args) else: call_task("pavelib.servers.{task_name}".format(task_name=task_name), options=options) - expected_messages = [] + expected_messages = options.get("expected_messages", []) expected_settings = settings if settings else "devstack" expected_asset_settings = asset_settings if asset_settings else expected_settings if is_optimized: diff --git a/pavelib/quality.py b/pavelib/quality.py index c0260df408..d05242620e 100644 --- a/pavelib/quality.py +++ b/pavelib/quality.py @@ -373,7 +373,9 @@ def run_quality(options): # Directory to put the diff reports in. # This makes the folder if it doesn't already exist. dquality_dir = (Env.REPORT_DIR / "diff_quality").makedirs_p() - diff_quality_percentage_failure = False + + # Save the pass variable. It will be set to false later if failures are detected. + diff_quality_percentage_pass = True def _pep8_output(count, violations_list, is_html=False): """ @@ -421,20 +423,20 @@ def run_quality(options): f.write(_pep8_output(count, violations_list, is_html=True)) if count > 0: - diff_quality_percentage_failure = True + diff_quality_percentage_pass = False # ----- Set up for diff-quality pylint call ----- # Set the string, if needed, to be used for the diff-quality --compare-branch switch. compare_branch = getattr(options, 'compare_branch', None) - compare_branch_string = '' + compare_branch_string = u'' if compare_branch: - compare_branch_string = '--compare-branch={0}'.format(compare_branch) + compare_branch_string = u'--compare-branch={0}'.format(compare_branch) # Set the string, if needed, to be used for the diff-quality --fail-under switch. diff_threshold = int(getattr(options, 'percentage', -1)) - percentage_string = '' + percentage_string = u'' if diff_threshold > -1: - percentage_string = '--fail-under={0}'.format(diff_threshold) + percentage_string = u'--fail-under={0}'.format(diff_threshold) # Generate diff-quality html report for pylint, and print to console # If pylint reports exist, use those @@ -448,28 +450,61 @@ def run_quality(options): "common:common/djangoapps:common/lib" ) + # run diff-quality for pylint. + if not run_diff_quality( + violations_type="pylint", + prefix=pythonpath_prefix, + reports=pylint_reports, + percentage_string=percentage_string, + branch_string=compare_branch_string, + dquality_dir=dquality_dir + ): + diff_quality_percentage_pass = False + + # run diff-quality for jshint. + if not run_diff_quality( + violations_type="jshint", + prefix=pythonpath_prefix, + reports=pylint_reports, + percentage_string=percentage_string, + branch_string=compare_branch_string, + dquality_dir=dquality_dir + ): + diff_quality_percentage_pass = False + + # If one of the quality runs fails, then paver exits with an error when it is finished + if not diff_quality_percentage_pass: + raise BuildFailure("Diff-quality failure(s).") + + +def run_diff_quality( + violations_type=None, prefix=None, reports=None, percentage_string=None, branch_string=None, dquality_dir=None +): + """ + This executes the diff-quality commandline tool for the given violation type (e.g., pylint, jshint). + If diff-quality fails due to quality issues, this method returns False. + + """ try: sh( - "{pythonpath_prefix} diff-quality --violations=pylint " - "{pylint_reports} {percentage_string} {compare_branch_string} " - "--html-report {dquality_dir}/diff_quality_pylint.html ".format( - pythonpath_prefix=pythonpath_prefix, - pylint_reports=pylint_reports, + "{pythonpath_prefix} diff-quality --violations={type} " + "{reports} {percentage_string} {compare_branch_string} " + "--html-report {dquality_dir}/diff_quality_{type}.html ".format( + type=violations_type, + pythonpath_prefix=prefix, + reports=reports, percentage_string=percentage_string, - compare_branch_string=compare_branch_string, + compare_branch_string=branch_string, dquality_dir=dquality_dir, ) ) + return True except BuildFailure, error_message: if is_percentage_failure(error_message): - diff_quality_percentage_failure = True + return False else: raise BuildFailure(error_message) - # If one of the diff-quality runs fails, then paver exits with an error when it is finished - if diff_quality_percentage_failure: - raise BuildFailure("Diff-quality failure(s).") - def is_percentage_failure(error_message): """ diff --git a/pavelib/servers.py b/pavelib/servers.py index d2b0615e7f..7c0d778bbf 100644 --- a/pavelib/servers.py +++ b/pavelib/servers.py @@ -135,6 +135,7 @@ def devstack(args): if args.optimized: settings = OPTIMIZED_SETTINGS asset_settings = OPTIMIZED_ASSETS_SETTINGS + sh(django_cmd('cms', settings, 'reindex_course', '--setup')) run_server( args.system[0], fast=args.fast, diff --git a/pavelib/utils/test/suites/nose_suite.py b/pavelib/utils/test/suites/nose_suite.py index 5dab89cdbc..7531ebf48f 100644 --- a/pavelib/utils/test/suites/nose_suite.py +++ b/pavelib/utils/test/suites/nose_suite.py @@ -145,20 +145,23 @@ class SystemTestSuite(NoseTestSuite): # django-nose will import them early in the test process, # thereby making sure that we load any django models that are # only defined in test files. - default_test_id = "{system}/djangoapps/* common/djangoapps/* openedx/core/djangoapps/*".format( - system=self.root + default_test_id = ( + "{system}/djangoapps/*" + " common/djangoapps/*" + " openedx/core/djangoapps/*" + " openedx/tests/*" ) if self.root in ('lms', 'cms'): - default_test_id += " {system}/lib/*".format(system=self.root) + default_test_id += " {system}/lib/*" if self.root == 'lms': - default_test_id += " {system}/tests.py".format(system=self.root) + default_test_id += " {system}/tests.py" if self.root == 'cms': - default_test_id += " {system}/tests/*".format(system=self.root) + default_test_id += " {system}/tests/*" - return default_test_id + return default_test_id.format(system=self.root) class LibTestSuite(NoseTestSuite): diff --git a/pylintrc b/pylintrc index 680947da4a..5d39a610cb 100644 --- a/pylintrc +++ b/pylintrc @@ -48,6 +48,7 @@ disable = too-many-branches, too-many-arguments, too-many-locals, + unused-wildcard-import, duplicate-code [REPORTS] @@ -151,4 +152,4 @@ int-import-graph = [EXCEPTIONS] overgeneral-exceptions = Exception -# 0f5810dfd8c52cdd91c425550319ae6040a8fe3e +# 6a610602e4c093047ed189c9a0d4ba796c7d1622 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 25e85d6b03..800190cca5 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -43,7 +43,7 @@ glob2==0.3 gunicorn==0.17.4 httpretty==0.8.3 lazy==1.1 -mako==0.9.1 +mako==1.0.2 Markdown==2.2.1 --allow-external meliae --allow-unverified meliae @@ -61,7 +61,7 @@ pycrypto>=2.6 pygments==2.0.1 pygraphviz==1.1 PyJWT==1.0.1 -pymongo==2.7.2 +pymongo==2.8.1 pyparsing==2.0.1 python-memcached==1.48 python-openid==2.2.5 @@ -75,7 +75,7 @@ requests-oauthlib==0.4.1 scipy==0.14.0 Shapely==1.2.16 singledispatch==3.4.0.2 -sorl-thumbnail==11.12 +sorl-thumbnail==12.3 sortedcontainers==0.9.2 South==1.0.1 stevedore==0.14.1 @@ -127,9 +127,9 @@ bok-choy==0.4.3 chrono==1.0.2 coverage==3.7.1 ddt==0.8.0 -diff-cover==0.7.3 +diff-cover==0.8.0 django-crum==0.5 -django_nose==1.3 +django_nose==1.4.1 factory_boy==2.2.1 flaky==2.0.3 freezegun==0.1.11 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 732e7253f8..505a45fbcc 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -33,7 +33,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx # Our libraries: --e git+https://github.com/edx/XBlock.git@d1ff8cf31a9b94916ce06ba06d4176bd72e15768#egg=XBlock +-e git+https://github.com/edx/XBlock.git@32fca2a954745315be97b91ef0d5ad4eb38cf365#egg=XBlock -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking @@ -49,15 +49,15 @@ git+https://github.com/edx/edx-oauth2-provider.git@0.5.6#egg=oauth2-provider==0. -e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock -e git+https://github.com/edx/edx-search.git@release-2015-07-22#egg=edx-search -e git+https://github.com/edx/edx-milestones.git@9b44a37edc3d63a23823c21a63cdd53ef47a7aa4#egg=edx-milestones -git+https://github.com/edx/edx-lint.git@b109a40c61277c52dcb396bf15e33755f5dbf5fa#egg=edx_lint==0.2.4 +git+https://github.com/edx/edx-lint.git@178819aae155f8f14db4ebb6866c867fb17d5000#egg=edx_lint==0.2.6 -e git+https://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive --e git+https://github.com/edx/edx-reverification-block.git@30fcf2fea305ed6649adcee9c831afaefba635c5#egg=edx-reverification-block +-e git+https://github.com/edx/edx-reverification-block.git@5e77525cab256a20a0cf182fcf5471369b284ff1#egg=edx-reverification-block git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-client==1.1.0 -e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client --e git+https://github.com/edx/edx-organizations.git@release-2015-08-03#egg=edx-organizations +-e git+https://github.com/edx/edx-organizations.git@release-2015-08-31#egg=edx-organizations -git+https://github.com/edx/edx-proctoring.git@0.7.2#egg=edx-proctoring==0.7.2 +git+https://github.com/edx/edx-proctoring.git@0.8.3#egg=edx-proctoring==0.8.3 # Third Party XBlocks -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index 80dd3c6851..bcd0035719 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -11,7 +11,7 @@ set -e ############################################################################### # Violations thresholds for failing the build -export PYLINT_THRESHOLD=6175 +export PYLINT_THRESHOLD=5999 export JSHINT_THRESHOLD=3700 doCheckVars() { diff --git a/scripts/circle-ci-tests.sh b/scripts/circle-ci-tests.sh index 8840c1485f..f8cb422b4a 100755 --- a/scripts/circle-ci-tests.sh +++ b/scripts/circle-ci-tests.sh @@ -44,13 +44,14 @@ case $CIRCLE_NODE_INDEX in # fails and aborts the job because nothing is displayed for > 10 minutes. paver run_pylint -l $PYLINT_THRESHOLD | tee pylint.log || EXIT=1 - # Run quality task. Pass in the 'fail-under' percentage to diff-quality - paver run_quality -p 100 || EXIT=1 - mkdir -p reports echo "Finding jshint violations and storing report..." PATH=$PATH:node_modules/.bin paver run_jshint -l $JSHINT_THRESHOLD > jshint.log || { cat jshint.log; EXIT=1; } + + # Run quality task. Pass in the 'fail-under' percentage to diff-quality + paver run_quality -p 100 || EXIT=1 + echo "Running code complexity report (python)." paver run_complexity > reports/code_complexity.log || echo "Unable to calculate code complexity. Ignoring error." diff --git a/scripts/generic-ci-tests.sh b/scripts/generic-ci-tests.sh index c2ab5056e2..f072c48fd6 100755 --- a/scripts/generic-ci-tests.sh +++ b/scripts/generic-ci-tests.sh @@ -67,8 +67,6 @@ case "$TEST_SUITE" in paver run_pep8 > pep8.log || { cat pep8.log; EXIT=1; } echo "Finding pylint violations and storing in report..." paver run_pylint -l $PYLINT_THRESHOLD || { cat pylint.log; EXIT=1; } - # Run quality task. Pass in the 'fail-under' percentage to diff-quality - paver run_quality -p 100 || EXIT=1 mkdir -p reports echo "Finding jshint violations and storing report..." @@ -76,6 +74,9 @@ case "$TEST_SUITE" in paver run_jshint -l $JSHINT_THRESHOLD > jshint.log || { cat jshint.log; EXIT=1; } echo "Running code complexity report (python)." paver run_complexity > reports/code_complexity.log || echo "Unable to calculate code complexity. Ignoring error." + # Run quality task. Pass in the 'fail-under' percentage to diff-quality + paver run_quality -p 100 || EXIT=1 + # Need to create an empty test result so the post-build # action doesn't fail the build. cat > reports/quality.xml <