diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py index 9a6340c2bf..f691ef5191 100644 --- a/common/djangoapps/course_groups/cohorts.py +++ b/common/djangoapps/course_groups/cohorts.py @@ -6,16 +6,69 @@ forums, and to the cohort admin views. import logging import random +from django.db.models.signals import post_save, m2m_changed +from django.dispatch import receiver from django.http import Http404 from django.utils.translation import ugettext as _ from courseware import courses +from eventtracking import tracker from student.models import get_user_by_username_or_email from .models import CourseUserGroup log = logging.getLogger(__name__) +@receiver(post_save, sender=CourseUserGroup) +def _cohort_added(sender, **kwargs): + """Emits a tracking log event each time a cohort is created""" + instance = kwargs["instance"] + if kwargs["created"] and instance.group_type == CourseUserGroup.COHORT: + tracker.emit( + "edx.cohort.created", + {"cohort_id": instance.id, "cohort_name": instance.name} + ) + + +@receiver(m2m_changed, sender=CourseUserGroup.users.through) +def _cohort_membership_changed(sender, **kwargs): + """Emits a tracking log event each time cohort membership is modified""" + def get_event_iter(user_id_iter, cohort_iter): + return ( + {"cohort_id": cohort.id, "cohort_name": cohort.name, "user_id": user_id} + for user_id in user_id_iter + for cohort in cohort_iter + ) + + action = kwargs["action"] + instance = kwargs["instance"] + pk_set = kwargs["pk_set"] + reverse = kwargs["reverse"] + + if action == "post_add": + event_name = "edx.cohort.user_added" + elif action in ["post_remove", "pre_clear"]: + event_name = "edx.cohort.user_removed" + else: + return + + if reverse: + user_id_iter = [instance.id] + if action == "pre_clear": + cohort_iter = instance.course_groups.filter(group_type=CourseUserGroup.COHORT) + else: + cohort_iter = CourseUserGroup.objects.filter(pk__in=pk_set, group_type=CourseUserGroup.COHORT) + else: + cohort_iter = [instance] if instance.group_type == CourseUserGroup.COHORT else [] + if action == "pre_clear": + user_id_iter = (user.id for user in instance.users.all()) + else: + user_id_iter = pk_set + + for event in get_event_iter(user_id_iter, cohort_iter): + tracker.emit(event_name, event) + + # A 'default cohort' is an auto-cohort that is automatically created for a course if no auto_cohort_groups have been # specified. It is intended to be used in a cohorted-course for users who have yet to be assigned to a cohort. # Note 1: If an administrator chooses to configure a cohort with the same name, the said cohort will be used as @@ -258,19 +311,16 @@ def add_cohort(course_key, name): except Http404: raise ValueError("Invalid course_key") - return CourseUserGroup.objects.create( + cohort = CourseUserGroup.objects.create( course_id=course.id, group_type=CourseUserGroup.COHORT, name=name ) - - -class CohortConflict(Exception): - """ - Raised when user to be added is already in another cohort in same course. - """ - pass - + tracker.emit( + "edx.cohort.creation_requested", + {"cohort_name": cohort.name, "cohort_id": cohort.id} + ) + return cohort def add_user_to_cohort(cohort, username_or_email): """ @@ -288,7 +338,8 @@ def add_user_to_cohort(cohort, username_or_email): ValueError if user already present in this cohort. """ user = get_user_by_username_or_email(username_or_email) - previous_cohort = None + previous_cohort_name = None + previous_cohort_id = None course_cohorts = CourseUserGroup.objects.filter( course_id=cohort.course_id, @@ -302,22 +353,20 @@ def add_user_to_cohort(cohort, username_or_email): cohort_name=cohort.name )) else: - previous_cohort = course_cohorts[0].name - course_cohorts[0].users.remove(user) + previous_cohort = course_cohorts[0] + previous_cohort.users.remove(user) + previous_cohort_name = previous_cohort.name + previous_cohort_id = previous_cohort.id + tracker.emit( + "edx.cohort.user_add_requested", + { + "user_id": user.id, + "cohort_id": cohort.id, + "cohort_name": cohort.name, + "previous_cohort_id": previous_cohort_id, + "previous_cohort_name": previous_cohort_name, + } + ) cohort.users.add(user) - return (user, previous_cohort) - - -def delete_empty_cohort(course_key, name): - """ - Remove an empty cohort. Raise ValueError if cohort is not empty. - """ - cohort = get_cohort_by_name(course_key, name) - if cohort.users.exists(): - raise ValueError(_("You cannot delete non-empty cohort {cohort_name} in course {course_key}").format( - cohort_name=name, - course_key=course_key - )) - - cohort.delete() + return (user, previous_cohort_name) diff --git a/common/djangoapps/course_groups/tests/helpers.py b/common/djangoapps/course_groups/tests/helpers.py index 8b19efa379..94925132e3 100644 --- a/common/djangoapps/course_groups/tests/helpers.py +++ b/common/djangoapps/course_groups/tests/helpers.py @@ -4,6 +4,7 @@ Helper methods for testing cohorts. from factory import post_generation, Sequence from factory.django import DjangoModelFactory from course_groups.models import CourseUserGroup +from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum @@ -15,7 +16,7 @@ class CohortFactory(DjangoModelFactory): FACTORY_FOR = CourseUserGroup name = Sequence("cohort{}".format) - course_id = "dummy_id" + course_id = SlashSeparatedCourseKey("dummy", "dummy", "dummy") group_type = CourseUserGroup.COHORT @post_generation diff --git a/common/djangoapps/course_groups/tests/test_cohorts.py b/common/djangoapps/course_groups/tests/test_cohorts.py index 16d971dded..7e87ee4ba9 100644 --- a/common/djangoapps/course_groups/tests/test_cohorts.py +++ b/common/djangoapps/course_groups/tests/test_cohorts.py @@ -4,6 +4,7 @@ from django.conf import settings from django.http import Http404 from django.test.utils import override_settings +from mock import call, patch from student.models import CourseEnrollment from student.tests.factories import UserFactory @@ -25,6 +26,103 @@ TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'} TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING) +@patch("course_groups.cohorts.tracker") +class TestCohortSignals(django.test.TestCase): + def setUp(self): + self.course_key = SlashSeparatedCourseKey("dummy", "dummy", "dummy") + + def test_cohort_added(self, mock_tracker): + # Add cohort + cohort = CourseUserGroup.objects.create( + name="TestCohort", + course_id=self.course_key, + group_type=CourseUserGroup.COHORT + ) + mock_tracker.emit.assert_called_with( + "edx.cohort.created", + {"cohort_id": cohort.id, "cohort_name": cohort.name} + ) + mock_tracker.reset_mock() + + # Modify existing cohort + cohort.name = "NewName" + cohort.save() + self.assertFalse(mock_tracker.called) + + # Add non-cohort group + CourseUserGroup.objects.create( + name="TestOtherGroupType", + course_id=self.course_key, + group_type="dummy" + ) + self.assertFalse(mock_tracker.called) + + def test_cohort_membership_changed(self, mock_tracker): + cohort_list = [CohortFactory() for _ in range(2)] + non_cohort = CourseUserGroup.objects.create( + name="dummy", + course_id=self.course_key, + group_type="dummy" + ) + user_list = [UserFactory() for _ in range(2)] + mock_tracker.reset_mock() + + def assert_events(event_name_suffix, user_list, cohort_list): + mock_tracker.emit.assert_has_calls([ + call( + "edx.cohort.user_" + event_name_suffix, + { + "user_id": user.id, + "cohort_id": cohort.id, + "cohort_name": cohort.name, + } + ) + for user in user_list for cohort in cohort_list + ]) + + # Add users to cohort + cohort_list[0].users.add(*user_list) + assert_events("added", user_list, cohort_list[:1]) + mock_tracker.reset_mock() + + # Remove users from cohort + cohort_list[0].users.remove(*user_list) + assert_events("removed", user_list, cohort_list[:1]) + mock_tracker.reset_mock() + + # Clear users from cohort + cohort_list[0].users.add(*user_list) + cohort_list[0].users.clear() + assert_events("removed", user_list, cohort_list[:1]) + mock_tracker.reset_mock() + + # Clear users from non-cohort group + non_cohort.users.add(*user_list) + non_cohort.users.clear() + self.assertFalse(mock_tracker.emit.called) + + # Add cohorts to user + user_list[0].course_groups.add(*cohort_list) + assert_events("added", user_list[:1], cohort_list) + mock_tracker.reset_mock() + + # Remove cohorts from user + user_list[0].course_groups.remove(*cohort_list) + assert_events("removed", user_list[:1], cohort_list) + mock_tracker.reset_mock() + + # Clear cohorts from user + user_list[0].course_groups.add(*cohort_list) + user_list[0].course_groups.clear() + assert_events("removed", user_list[:1], cohort_list) + mock_tracker.reset_mock() + + # Clear non-cohort groups from user + user_list[0].course_groups.add(non_cohort) + user_list[0].course_groups.clear() + self.assertFalse(mock_tracker.emit.called) + + @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestCohorts(django.test.TestCase): @@ -354,13 +452,18 @@ class TestCohorts(django.test.TestCase): lambda: cohorts.get_cohort_by_id(course.id, cohort.id) ) - def test_add_cohort(self): + @patch("course_groups.cohorts.tracker") + def test_add_cohort(self, mock_tracker): """ Make sure cohorts.add_cohort() properly adds a cohort to a course and handles errors. """ course = modulestore().get_course(self.toy_course_key) added_cohort = cohorts.add_cohort(course.id, "My Cohort") + mock_tracker.emit.assert_any_call( + "edx.cohort.creation_requested", + {"cohort_name": added_cohort.name, "cohort_id": added_cohort.id} + ) self.assertEqual(added_cohort.name, "My Cohort") self.assertRaises( @@ -372,7 +475,8 @@ class TestCohorts(django.test.TestCase): lambda: cohorts.add_cohort(SlashSeparatedCourseKey("course", "does_not", "exist"), "My Cohort") ) - def test_add_user_to_cohort(self): + @patch("course_groups.cohorts.tracker") + def test_add_user_to_cohort(self, mock_tracker): """ Make sure cohorts.add_user_to_cohort() properly adds a user to a cohort and handles errors. @@ -390,13 +494,32 @@ class TestCohorts(django.test.TestCase): cohorts.add_user_to_cohort(first_cohort, "Username"), (course_user, None) ) + mock_tracker.emit.assert_any_call( + "edx.cohort.user_add_requested", + { + "user_id": course_user.id, + "cohort_id": first_cohort.id, + "cohort_name": first_cohort.name, + "previous_cohort_id": None, + "previous_cohort_name": None, + } + ) # Should get (user, previous_cohort_name) when moved from one cohort to # another self.assertEqual( cohorts.add_user_to_cohort(second_cohort, "Username"), (course_user, "FirstCohort") ) - + mock_tracker.emit.assert_any_call( + "edx.cohort.user_add_requested", + { + "user_id": course_user.id, + "cohort_id": second_cohort.id, + "cohort_name": second_cohort.name, + "previous_cohort_id": first_cohort.id, + "previous_cohort_name": first_cohort.name, + } + ) # Error cases # Should get ValueError if user already in cohort self.assertRaises( @@ -408,34 +531,3 @@ class TestCohorts(django.test.TestCase): User.DoesNotExist, lambda: cohorts.add_user_to_cohort(first_cohort, "non_existent_username") ) - - def test_delete_empty_cohort(self): - """ - Make sure that cohorts.delete_empty_cohort() properly removes an empty cohort - for a given course. - """ - course = modulestore().get_course(self.toy_course_key) - user = UserFactory(username="Username", email="a@b.com") - empty_cohort = CohortFactory(course_id=course.id, name="EmptyCohort") - nonempty_cohort = CohortFactory(course_id=course.id, name="NonemptyCohort") - nonempty_cohort.users.add(user) - - cohorts.delete_empty_cohort(course.id, "EmptyCohort") - - # Make sure we cannot access the deleted cohort - self.assertRaises( - CourseUserGroup.DoesNotExist, - lambda: cohorts.get_cohort_by_id(course.id, empty_cohort.id) - ) - self.assertRaises( - ValueError, - lambda: cohorts.delete_empty_cohort(course.id, "NonemptyCohort") - ) - self.assertRaises( - CourseUserGroup.DoesNotExist, - lambda: cohorts.delete_empty_cohort(SlashSeparatedCourseKey('course', 'does_not', 'exist'), "EmptyCohort") - ) - self.assertRaises( - CourseUserGroup.DoesNotExist, - lambda: cohorts.delete_empty_cohort(course.id, "NonExistentCohort") - ) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 08402c38bf..de4f9fe80e 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -267,7 +267,7 @@ class CourseFields(object): ) cohort_config = Dict( display_name=_("Cohort Configuration"), - help=_("Cohorts are not currently supported by edX."), + help=_("Enter policy keys and values to enable the cohort feature, define automated student assignment to groups, or identify any course-wide discussion topics as private to cohort members."), scope=Scope.settings ) is_new = Boolean( diff --git a/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html b/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html index 0e133de9ff..94a1d8f729 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html +++ b/common/lib/xmodule/xmodule/js/fixtures/jsinput_problem.html @@ -1,6 +1,6 @@
This is a test message
") return course_email.id # pylint: disable=E1101 - def test_submit_bulk_email_all(self): - email_id = self._define_course_email() - instructor_task = submit_bulk_course_email(self.create_task_request(self.instructor), self.course.id, email_id) - - # test resubmitting, by updating the existing record: + def _test_resubmission(self, api_call): + """ + Tests the resubmission of an instructor task through the API. + The call to the API is a lambda expression passed via + `api_call`. Expects that the API call returns the resulting + InstructorTask object, and that its resubmission raises + `AlreadyRunningError`. + """ + instructor_task = api_call() instructor_task = InstructorTask.objects.get(id=instructor_task.id) # pylint: disable=E1101 instructor_task.task_state = PROGRESS instructor_task.save() - with self.assertRaises(AlreadyRunningError): - instructor_task = submit_bulk_course_email(self.create_task_request(self.instructor), self.course.id, email_id) + api_call() + + def test_submit_bulk_email_all(self): + email_id = self._define_course_email() + api_call = lambda: submit_bulk_course_email( + self.create_task_request(self.instructor), + self.course.id, + email_id + ) + 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), + self.course.id, + features=[] + ) + self._test_resubmission(api_call) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 99bc997a18..976022e93c 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -13,32 +13,32 @@ from mock import Mock, patch from django.conf import settings from django.test.testcases import TestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory -from instructor_task.tasks_helper import push_grades_to_s3 +from instructor_task.models import ReportStore +from instructor_task.tasks_helper import push_grades_to_s3, push_students_csv_to_s3, UPDATE_STATUS_SUCCEEDED -TEST_COURSE_ORG = 'edx' -TEST_COURSE_NAME = 'test_course' -TEST_COURSE_NUMBER = '1.23x' - - -@ddt.ddt -class TestInstructorGradeReport(TestCase): +class TestReport(ModuleStoreTestCase): """ - Tests that CSV grade report generation works. + Base class for testing CSV download tasks. """ def setUp(self): - self.course = CourseFactory.create(org=TEST_COURSE_ORG, - number=TEST_COURSE_NUMBER, - display_name=TEST_COURSE_NAME) + self.course = CourseFactory.create() def tearDown(self): if os.path.exists(settings.GRADES_DOWNLOAD['ROOT_PATH']): shutil.rmtree(settings.GRADES_DOWNLOAD['ROOT_PATH']) + +@ddt.ddt +class TestInstructorGradeReport(TestReport): + """ + Tests that CSV grade report generation works. + """ def create_student(self, username, email): student = UserFactory.create(username=username, email=email) CourseEnrollmentFactory.create(user=student, course_id=self.course.id) @@ -58,3 +58,18 @@ class TestInstructorGradeReport(TestCase): result = push_grades_to_s3(None, None, self.course.id, None, 'graded') #This assertion simply confirms that the generation completed with no errors self.assertEquals(result['succeeded'], result['attempted']) + + +class TestStudentReport(TestReport): + """ + Tests that CSV student profile report generation works. + """ + def test_success(self): + task_input = {'features': []} + with patch('instructor_task.tasks_helper._get_current_task'): + result = push_students_csv_to_s3(None, None, self.course.id, task_input, 'calculated') + report_store = ReportStore.from_config() + links = report_store.links_for(self.course.id) + + self.assertEquals(len(links), 1) + self.assertEquals(result, UPDATE_STATUS_SUCCEEDED) diff --git a/lms/djangoapps/notifier_api/serializers.py b/lms/djangoapps/notifier_api/serializers.py index db145768bf..5164ad812f 100644 --- a/lms/djangoapps/notifier_api/serializers.py +++ b/lms/djangoapps/notifier_api/serializers.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import User +from django.http import Http404 from rest_framework import serializers from course_groups.cohorts import is_course_cohorted @@ -47,16 +48,20 @@ class NotifierUserSerializer(serializers.ModelSerializer): for role in user.roles.all() for perm in role.permissions.all() if perm.name == "see_all_cohorts" } - return { - unicode(enrollment.course_id): { - "cohort_id": cohort_id_map.get(enrollment.course_id), - "see_all_cohorts": ( - enrollment.course_id in see_all_cohorts_set or - not is_course_cohorted(enrollment.course_id) - ), - } - for enrollment in user.courseenrollment_set.all() if enrollment.is_active - } + ret = {} + for enrollment in user.courseenrollment_set.all(): + if enrollment.is_active: + try: + ret[unicode(enrollment.course_id)] = { + "cohort_id": cohort_id_map.get(enrollment.course_id), + "see_all_cohorts": ( + enrollment.course_id in see_all_cohorts_set or + not is_course_cohorted(enrollment.course_id) + ), + } + except Http404: # is_course_cohorted raises this if course does not exist + pass + return ret class Meta: model = User diff --git a/lms/djangoapps/notifier_api/tests.py b/lms/djangoapps/notifier_api/tests.py index d741a4f940..69733ae7ad 100644 --- a/lms/djangoapps/notifier_api/tests.py +++ b/lms/djangoapps/notifier_api/tests.py @@ -10,6 +10,7 @@ from django_comment_common.models import Role, Permission from lang_pref import LANGUAGE_KEY from notification_prefs import NOTIFICATION_PREF_KEY from notifier_api.views import NotifierUsersViewSet +from opaque_keys.edx.locator import CourseLocator from student.models import CourseEnrollment from student.tests.factories import UserFactory, CourseEnrollmentFactory from user_api.models import UserPreference @@ -120,6 +121,14 @@ class NotifierUsersViewSetTest(UrlResetMixin, ModuleStoreTestCase): result = self._get_detail() self.assertEqual(result["course_info"], {}) + def test_course_info_non_existent_course_enrollment(self): + CourseEnrollmentFactory( + user=self.user, + course_id=CourseLocator(org="dummy", course="dummy", run="non_existent") + ) + result = self._get_detail() + self.assertEqual(result["course_info"], {}) + def test_preferences(self): lang_pref = UserPreferenceFactory( user=self.user, diff --git a/lms/envs/bok_choy.auth.json b/lms/envs/bok_choy.auth.json index 66ca730f5e..86d08d6029 100644 --- a/lms/envs/bok_choy.auth.json +++ b/lms/envs/bok_choy.auth.json @@ -46,6 +46,15 @@ "port": 27017, "user": "edxapp" }, + "EVENT_TRACKING_BACKENDS": { + "mongo": { + "ENGINE": "eventtracking.backends.mongodb.MongoBackend", + "OPTIONS": { + "database": "test", + "collection": "events" + } + } + }, "MODULESTORE": { "default": { "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 255efd92a5..ceedbd3899 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -62,6 +62,7 @@ class Thread(models.Model): if query_params.get('text'): search_query = query_params['text'] course_id = query_params['course_id'] + group_id = query_params['group_id'] if 'group_id' in query_params else None requested_page = params['page'] total_results = response.get('total_results') corrected_text = response.get('corrected_text') @@ -72,15 +73,17 @@ class Thread(models.Model): { 'query': search_query, 'corrected_text': corrected_text, + 'group_id': group_id, 'page': requested_page, 'total_results': total_results, } ) log.info( - 'forum_text_search query="{search_query}" corrected_text="{corrected_text}" course_id={course_id} page={requested_page} total_results={total_results}'.format( + u'forum_text_search query="{search_query}" corrected_text="{corrected_text}" course_id={course_id} group_id={group_id} page={requested_page} total_results={total_results}'.format( search_query=search_query, corrected_text=corrected_text, course_id=course_id, + group_id=group_id, requested_page=requested_page, total_results=total_results ) diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index 3ff456c871..4931e35c83 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -26,11 +26,11 @@ class DataDownload # response areas @$download = @$section.find '.data-download-container' @$download_display_text = @$download.find '.data-display-text' - @$download_display_table = @$download.find '.data-display-table' @$download_request_response_error = @$download.find '.request-response-error' - @$grades = @$section.find '.grades-download-container' - @$grades_request_response = @$grades.find '.request-response' - @$grades_request_response_error = @$grades.find '.request-response-error' + @$reports = @$section.find '.reports-download-container' + @$download_display_table = @$reports.find '.data-display-table' + @$reports_request_response = @$reports.find '.request-response' + @$reports_request_response_error = @$reports.find '.request-response-error' @report_downloads = new ReportDownloads(@$section) @instructor_tasks = new (PendingInstructorTasks()) @$section @@ -45,11 +45,22 @@ class DataDownload # this handler binds to both the download # and the csv button @$list_studs_csv_btn.click (e) => + @clear_display() + url = @$list_studs_csv_btn.data 'endpoint' # handle csv special case # redirect the document to the csv file. url += '/csv' - location.href = url + + $.ajax + dataType: 'json' + url: url + error: (std_ajax_err) => + @$reports_request_response_error.text gettext("Error generating student profile information. Please try again.") + $(".msg-error").css({"display":"block"}) + success: (data) => + @$reports_request_response.text data['status'] + $(".msg-confirm").css({"display":"block"}) @$list_studs_btn.click (e) => url = @$list_studs_btn.data 'endpoint' @@ -62,7 +73,7 @@ class DataDownload $.ajax dataType: 'json' url: url - error: std_ajax_err => + error: (std_ajax_err) => @clear_display() @$download_request_response_error.text gettext("Error getting student list.") success: (data) => @@ -89,7 +100,7 @@ class DataDownload $.ajax dataType: 'json' url: url - error: std_ajax_err => + error: (std_ajax_err) => @clear_display() @$download_request_response_error.text gettext("Error retrieving grading configuration.") success: (data) => @@ -105,11 +116,11 @@ class DataDownload $.ajax dataType: 'json' url: url - error: std_ajax_err => - @$grades_request_response_error.text gettext("Error generating grades. Please try again.") + error: (std_ajax_err) => + @$reports_request_response_error.text gettext("Error generating grades. Please try again.") $(".msg-error").css({"display":"block"}) success: (data) => - @$grades_request_response.text data['status'] + @$reports_request_response.text data['status'] $(".msg-confirm").css({"display":"block"}) # handler for when the section title is clicked. @@ -129,8 +140,8 @@ class DataDownload @$download_display_text.empty() @$download_display_table.empty() @$download_request_response_error.empty() - @$grades_request_response.empty() - @$grades_request_response_error.empty() + @$reports_request_response.empty() + @$reports_request_response_error.empty() # Clear any CSS styling from the request-response areas $(".msg-confirm").css({"display":"none"}) $(".msg-error").css({"display":"none"}) @@ -157,7 +168,7 @@ class ReportDownloads @create_report_downloads_table data.downloads else console.log "No reports ready for download" - error: std_ajax_err => console.error "Error finding report downloads" + error: (std_ajax_err) => console.error "Error finding report downloads" create_report_downloads_table: (report_downloads_data) -> @$report_downloads_table.empty() diff --git a/lms/static/js/views/cohorts.js b/lms/static/js/views/cohorts.js index c7cf01aeba..9f9131f316 100644 --- a/lms/static/js/views/cohorts.js +++ b/lms/static/js/views/cohorts.js @@ -7,7 +7,8 @@ 'change .cohort-select': 'onCohortSelected', 'click .action-create': 'showAddCohortForm', 'click .action-cancel': 'cancelAddCohortForm', - 'click .action-save': 'saveAddCohortForm' + 'click .action-save': 'saveAddCohortForm', + 'click .link-cross-reference': 'showSection' }, initialize: function(options) { @@ -170,6 +171,13 @@ event.preventDefault(); this.removeNotification(); this.onSync(); + }, + + showSection: function(event) { + event.preventDefault(); + var section = $(event.currentTarget).data("section"); + $(".instructor-nav .nav-item a[data-section='" + section + "']").click(); + $(window).scrollTop(0); } }); }).call(this, $, _, Backbone, gettext, interpolate_text, CohortEditorView, NotificationModel, NotificationView); diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index d530877f3f..7ae6a4fc6e 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -894,15 +894,12 @@ section.instructor-dashboard-content-2 { line-height: 1.3em; } - .data-download-container { + .reports-download-container { .data-display-table { .slickgrid { height: 400px; } } - } - - .grades-download-container { .report-downloads-table { .slickgrid { height: 300px; diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 23dac72409..e061d6117d 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -77,7 +77,7 @@ location.href = "${reverse('dashboard')}"; } else if (xhr.status == 403) { location.href = "${reverse('signin_user')}?course_id=" + - encodeURIComponont($("#unenroll_course_id").val()) + "&enrollment_action=unenroll"; + encodeURIComponent($("#unenroll_course_id").val()) + "&enrollment_action=unenroll"; } else { $('#unenroll_error').html( xhr.responseText ? xhr.responseText : "${_("An error occurred. Please try again later.")}" diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index 635ff9d7f0..1e36c0fc9b 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -397,7 +397,7 @@ ${'<% }); %>'}+ + <%= interpolate( + gettext('To review all student cohort group assignments, download course profile information on %(link_start)s the Data Download page. %(link_end)s'), + {link_start: '', link_end: ''}, + true + ) %> +
+${_("Click to generate a CSV file of all students enrolled in this course, along with profile information such as email address and username:")}
- - - - % if not disable_buttons: -${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}
- - %endif - -${_("Click to display the grading configuration for the course. The grading configuration is the breakdown of graded subsections of the course (such as exams and problem sets), and can be changed on the 'Grading' page (under 'Settings') in Studio.")}
@@ -29,27 +19,37 @@${_("For large courses, generating some reports can take several hours. When report generation is complete, a link that includes the date and time of generation appears in the table below. These reports are generated in the background, meaning it is OK to navigate away from this page while your report is generating.")}
+ +${_("Please be patient and do not click these buttons multiple times. Clicking these buttons multiple times will significantly slow the generation process.")}
+ +${_("Click to generate a CSV file of all students enrolled in this course, along with profile information such as email address and username:")}
+ + + + % if not disable_buttons: +${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}
+ + %endif + + %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: -${_("Click to generate a CSV grade report for all currently enrolled students. Links to generated reports appear in a table below when report generation is complete.")}
- -${_("For large courses, generating this report may take several hours. Please be patient and do not click the button multiple times. Clicking the button multiple times will significantly slow the grade generation process.")}
- -${_("The report is generated in the background, meaning it is OK to navigate away from this page while your report is generating.")}
- - - -${_("Click to generate a CSV grade report for all currently enrolled students.")}
%endif + + +${_("Reports Available for Download")}
- ${_("The grade reports listed below are generated each time the Generate Grade Report button is clicked. A link to each grade report remains available on this page, identified by the UTC date and time of generation. Grade reports are not deleted, so you will always be able to access previously generated reports from this page.")} + ${_("The reports listed below are available for download. A link to every report remains available on this page, identified by the UTC date and time of generation. Reports are not deleted, so you will always be able to access previously generated reports from this page.")}
%if settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'): diff --git a/lms/templates/problem.html b/lms/templates/problem.html index 40b651d9c0..090e260aff 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -7,7 +7,7 @@ -