diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79de8c09f4..6f004b9657 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,10 @@ Studio: Add drag-and-drop support to the container page. STUD-1309. Common: Add extensible third-party auth module. +LMS: Switch default instructor dashboard to the new (formerly "beta") + instructor dashboard. Puts the old (now "legacy") dash behind a feature flag. + LMS-1296 + Blades: Handle situation if no response were sent from XQueue to LMS in Matlab problem after Run Code button press. BLD-994. diff --git a/lms/djangoapps/bulk_email/tests/test_course_optout.py b/lms/djangoapps/bulk_email/tests/test_course_optout.py index 23bcbde954..f129c6f30c 100644 --- a/lms/djangoapps/bulk_email/tests/test_course_optout.py +++ b/lms/djangoapps/bulk_email/tests/test_course_optout.py @@ -38,6 +38,12 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): self.client.login(username=self.student.username, password="test") + self.send_mail_url = reverse('send_email', kwargs={'course_id': self.course.id}) + self.success_content = { + 'course_id': self.course.id, + 'success': True, + } + def tearDown(self): """ Undo all patches. @@ -48,18 +54,12 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): """Navigate to the instructor dash's email view""" # Pull up email view on instructor dashboard url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + # Response loads the whole instructor dashboard, so no need to explicitly + # navigate to a particular email section response = self.client.get(url) - email_link = 'Email' + email_section = '
' # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False - self.assertTrue(email_link in response.content) - - # Select the Email view of the instructor dash - session = self.client.session - session[u'idash_mode:{0}'.format(self.course.location.course_id)] = 'Email' - session.save() - response = self.client.get(url) - selected_email_link = 'Email' - self.assertTrue(selected_email_link in response.content) + self.assertTrue(email_section in response.content) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False}) def test_optout_course(self): @@ -77,15 +77,14 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): self.client.login(username=self.instructor.username, password="test") self.navigate_to_email_view() - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - response = self.client.post(url, test_email) - self.assertContains(response, "Your email was successfully queued for sending.") + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) # Assert that self.student.email not in mail.to, outbox should be empty self.assertEqual(len(mail.outbox), 0) @@ -106,16 +105,14 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): self.client.login(username=self.instructor.username, password="test") self.navigate_to_email_view() - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - response = self.client.post(url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) # Assert that self.student.email in mail.to self.assertEqual(len(mail.outbox), 1) diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index d1b8e380e9..bcfcbba721 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -2,6 +2,8 @@ """ Unit tests for sending course email """ +import json + from mock import patch from django.conf import settings @@ -70,18 +72,17 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): # Pull up email view on instructor dashboard self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + # Response loads the whole instructor dashboard, so no need to explicitly + # navigate to a particular email section response = self.client.get(self.url) - email_link = 'Email' + email_section = '
' # If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False - self.assertTrue(email_link in response.content) - - # Select the Email view of the instructor dash - session = self.client.session - session[u'idash_mode:{0}'.format(self.course.location.course_id)] = 'Email' - session.save() - response = self.client.get(self.url) - selected_email_link = 'Email' - self.assertTrue(selected_email_link in response.content) + self.assertTrue(email_section in response.content) + self.send_mail_url = reverse('send_email', kwargs={'course_id': self.course.id}) + self.success_content = { + 'course_id': self.course.id, + 'success': True, + } def tearDown(self): """ @@ -96,12 +97,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): """ test_email = { 'action': 'Send email', - 'to_option': 'myself', + 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } - response = self.client.post(self.url, test_email) - self.assertContains(response, "Email is not enabled for this course.") + response = self.client.post(self.send_mail_url, test_email) + # We should get back a HttpResponseForbidden (status code 403) + self.assertContains(response, "Email is not enabled for this course.", status_code=403) def test_send_to_self(self): """ @@ -110,15 +112,16 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): # Now we know we have pulled up the instructor dash's email view # (in the setUp method), we can test sending an email. test_email = { - 'action': 'Send email', - 'to_option': 'myself', + 'action': 'send', + 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) + # Check that outbox is as expected self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox[0].to), 1) self.assertEquals(mail.outbox[0].to[0], self.instructor.email) @@ -135,13 +138,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): # (in the setUp method), we can test sending an email. test_email = { 'action': 'Send email', - 'to_option': 'staff', + 'send_to': 'staff', 'subject': 'test subject for staff', 'message': 'test message for subject' } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) # the 1 is for the instructor in this test and others self.assertEquals(len(mail.outbox), 1 + len(self.staff)) @@ -159,13 +162,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students)) self.assertItemsEqual( @@ -183,13 +186,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): uni_subject = u'téśt śúbjéćt főŕ áĺĺ' test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': uni_subject, 'message': 'test message for all' } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students)) self.assertItemsEqual( @@ -211,13 +214,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): uni_message = u'ẗëṡẗ ṁëṡṡäġë ḟöṛ äḷḷ イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ fоѓ аll' test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': uni_message } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students)) self.assertItemsEqual( @@ -242,13 +245,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - response = self.client.post(self.url, test_email) - - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students)) @@ -280,12 +283,14 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase): test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - response = self.client.post(self.url, test_email) - self.assertContains(response, "Your email was successfully queued for sending.") + # Post the email to the instructor dashboard API + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) + self.assertEquals(mock_factory.emails_sent, 1 + len(self.staff) + len(self.students) + LARGE_NUM_EMAILS - len(optouts)) outbox_contents = [e.to[0] for e in mail.outbox] diff --git a/lms/djangoapps/bulk_email/tests/test_err_handling.py b/lms/djangoapps/bulk_email/tests/test_err_handling.py index f7b8b32d1f..c6b54391b2 100644 --- a/lms/djangoapps/bulk_email/tests/test_err_handling.py +++ b/lms/djangoapps/bulk_email/tests/test_err_handling.py @@ -2,6 +2,8 @@ """ Unit tests for handling email sending errors """ +import json + from itertools import cycle from mock import patch from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError @@ -53,6 +55,11 @@ class TestEmailErrors(ModuleStoreTestCase): # load initial content (since we don't run migrations as part of tests): call_command("loaddata", "course_email_template.json") self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + self.send_mail_url = reverse('send_email', kwargs={'course_id': self.course.id}) + self.success_content = { + 'course_id': self.course.id, + 'success': True, + } def tearDown(self): patch.stopall() @@ -66,15 +73,16 @@ class TestEmailErrors(ModuleStoreTestCase): get_conn.return_value.send_messages.side_effect = SMTPDataError(455, "Throttling: Sending rate exceeded") test_email = { 'action': 'Send email', - 'to_option': 'myself', + 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } - self.client.post(self.url, test_email) + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) # Test that we retry upon hitting a 4xx error self.assertTrue(retry.called) - (_, kwargs) = retry.call_args + (__, kwargs) = retry.call_args exc = kwargs['exc'] self.assertIsInstance(exc, SMTPDataError) @@ -94,11 +102,12 @@ class TestEmailErrors(ModuleStoreTestCase): test_email = { 'action': 'Send email', - 'to_option': 'all', + 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } - self.client.post(self.url, test_email) + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) # We shouldn't retry when hitting a 5xx error self.assertFalse(retry.called) @@ -118,14 +127,15 @@ class TestEmailErrors(ModuleStoreTestCase): get_conn.return_value.open.side_effect = SMTPServerDisconnected(425, "Disconnecting") test_email = { 'action': 'Send email', - 'to_option': 'myself', + 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } - self.client.post(self.url, test_email) + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertTrue(retry.called) - (_, kwargs) = retry.call_args + (__, kwargs) = retry.call_args exc = kwargs['exc'] self.assertIsInstance(exc, SMTPServerDisconnected) @@ -139,14 +149,15 @@ class TestEmailErrors(ModuleStoreTestCase): test_email = { 'action': 'Send email', - 'to_option': 'myself', + 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } - self.client.post(self.url, test_email) + response = self.client.post(self.send_mail_url, test_email) + self.assertEquals(json.loads(response.content), self.success_content) self.assertTrue(retry.called) - (_, kwargs) = retry.call_args + (__, kwargs) = retry.call_args exc = kwargs['exc'] self.assertIsInstance(exc, SMTPConnectError) @@ -162,7 +173,7 @@ class TestEmailErrors(ModuleStoreTestCase): task_input = {"email_id": -1} with self.assertRaises(CourseEmail.DoesNotExist): perform_delegate_email_batches(entry.id, course_id, task_input, "action_name") # pylint: disable=E1101 - ((log_str, _, email_id), _) = mock_log.warning.call_args + ((log_str, __, email_id), __) = mock_log.warning.call_args self.assertTrue(mock_log.warning.called) self.assertIn('Failed to get CourseEmail with id', log_str) self.assertEqual(email_id, -1) diff --git a/lms/djangoapps/class_dashboard/test/test_dashboard_data.py b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py similarity index 97% rename from lms/djangoapps/class_dashboard/test/test_dashboard_data.py rename to lms/djangoapps/class_dashboard/tests/test_dashboard_data.py index e53d373b75..a011ee6dce 100644 --- a/lms/djangoapps/class_dashboard/test/test_dashboard_data.py +++ b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py @@ -253,17 +253,6 @@ class TestGetProblemGradeDistribution(ModuleStoreTestCase): b_section_has_problem = get_array_section_has_problem(self.course.id) self.assertEquals(b_section_has_problem[0], True) - def test_dashboard(self): - - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) - response = self.client.post( - url, - { - 'idash_mode': 'Metrics' - } - ) - self.assertContains(response, '

Course Statistics At A Glance

') - def test_has_instructor_access_for_class(self): """ Test for instructor access diff --git a/lms/djangoapps/class_dashboard/test/test_views.py b/lms/djangoapps/class_dashboard/tests/test_views.py similarity index 100% rename from lms/djangoapps/class_dashboard/test/test_views.py rename to lms/djangoapps/class_dashboard/tests/test_views.py diff --git a/lms/djangoapps/courseware/features/courseware_common.py b/lms/djangoapps/courseware/features/courseware_common.py index 1c836f8674..fdc6dd2617 100644 --- a/lms/djangoapps/courseware/features/courseware_common.py +++ b/lms/djangoapps/courseware/features/courseware_common.py @@ -1,5 +1,6 @@ # pylint: disable=C0111 # pylint: disable=W0621 +# pylint: disable=unused-argument from lettuce import world, step @@ -14,6 +15,11 @@ def i_click_on_the_tab(step, tab_text): world.click_link(tab_text) +@step('I click on the "([^"]*)" link$') +def i_click_on_the_link(step, link_text): + world.click_link(link_text) + + @step('I visit the courseware URL$') def i_visit_the_course_info_url(step): world.visit('/courses/MITx/6.002x/2012_Fall/courseware') diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature index eb888daede..e26647de32 100644 --- a/lms/djangoapps/courseware/features/lti.feature +++ b/lms/djangoapps/courseware/features/lti.feature @@ -51,7 +51,8 @@ Feature: LMS.LTI component Then I see text "Problem Scores: 5/10" And I see graph with total progress "5%" Then I click on the "Instructor" tab - And I click on the "Gradebook" tab + And I click on the "Student Admin" tab + And I click on the "View Gradebook" link And I see in the gradebook table that "HW" is "50" And I see in the gradebook table that "Total" is "5" @@ -88,7 +89,8 @@ Feature: LMS.LTI component Then I see text "Problem Scores: 8/10" And I see graph with total progress "8%" Then I click on the "Instructor" tab - And I click on the "Gradebook" tab + And I click on the "Student Admin" tab + And I click on the "View Gradebook" link And I see in the gradebook table that "HW" is "80" And I see in the gradebook table that "Total" is "8" And I visit the LTI component @@ -113,7 +115,8 @@ Feature: LMS.LTI component Then I see text "Problem Scores: 0/10" And I see graph with total progress "0%" Then I click on the "Instructor" tab - And I click on the "Gradebook" tab + And I click on the "Student Admin" tab + And I click on the "View Gradebook" link And I see in the gradebook table that "HW" is "0" And I see in the gradebook table that "Total" is "0" diff --git a/lms/djangoapps/instructor/features/bulk_email.py b/lms/djangoapps/instructor/features/bulk_email.py index 821e6c9f73..a3981f0e61 100644 --- a/lms/djangoapps/instructor/features/bulk_email.py +++ b/lms/djangoapps/instructor/features/bulk_email.py @@ -117,7 +117,6 @@ def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument # Go to the email section of the instructor dash world.visit('/courses/edx/888/Bulk_Email_Test_Course') world.css_click('a[href="/courses/edx/888/Bulk_Email_Test_Course/instructor"]') - world.css_click('div.beta-button-wrapper>a.beta-button') world.css_click('a[data-section="send_email"]') # Select the recipient diff --git a/lms/djangoapps/instructor/features/common.py b/lms/djangoapps/instructor/features/common.py index d6c23a1518..c2074ad493 100644 --- a/lms/djangoapps/instructor/features/common.py +++ b/lms/djangoapps/instructor/features/common.py @@ -77,7 +77,6 @@ def go_to_section(section_name): # course_info, membership, student_admin, data_download, analytics, send_email world.visit('/courses/edx/999/Test_Course') world.css_click('a[href="/courses/edx/999/Test_Course/instructor"]') - world.css_click('div.beta-button-wrapper>a.beta-button') world.css_click('a[data-section="{0}"]'.format(section_name)) diff --git a/lms/djangoapps/instructor/tests/test_email.py b/lms/djangoapps/instructor/tests/test_email.py index 6a2b71f2e8..8c9bc2542b 100644 --- a/lms/djangoapps/instructor/tests/test_email.py +++ b/lms/djangoapps/instructor/tests/test_email.py @@ -34,7 +34,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase): self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) # URL for email view self.email_link = 'Email' @@ -122,7 +122,7 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase): self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard_2', kwargs={'course_id': self.course_name}) + self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course_name}) # URL for email view self.email_link = 'Email' diff --git a/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py index b3a329ee05..60b05440e3 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py @@ -52,7 +52,7 @@ class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCas def test_download_anon_csv(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) with patch('instructor.views.legacy.unique_id_for_user') as mock_unique: mock_unique.return_value = 42 diff --git a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py index a03f029ff3..f5be4e8a67 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_download_csv.py @@ -49,7 +49,7 @@ class TestInstructorDashboardGradeDownloadCSV(ModuleStoreTestCase, LoginEnrollme def test_download_grades_csv(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) msg = "url = {0}\n".format(url) response = self.client.post(url, {'action': 'Download CSV of all student grades for this course'}) msg += "instructor dashboard download csv grades: response = '{0}'\n".format(response) diff --git a/lms/djangoapps/instructor/tests/test_legacy_email.py b/lms/djangoapps/instructor/tests/test_legacy_email.py index 823d112b3f..90ff66bb75 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_email.py +++ b/lms/djangoapps/instructor/tests/test_legacy_email.py @@ -32,7 +32,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): self.client.login(username=instructor.username, password="test") # URL for instructor dash - self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + self.url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id}) # URL for email view self.email_link = 'Email' diff --git a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py index 2a3b29c742..4efd6f88b2 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py @@ -52,7 +52,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Un-enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post( url, { @@ -84,7 +84,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student1_1@test.com, student1_2@test.com', 'auto_enroll': 'on'}) # Check the page output @@ -129,7 +129,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student0@test.com', 'auto_enroll': 'on'}) self.assertContains(response, 'student0@test.com') self.assertContains(response, 'already enrolled') @@ -142,7 +142,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) course = self.course # Run the Enroll students command - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'}) # Check the page output @@ -199,7 +199,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) # Create activated, but not enrolled, user UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled') - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) # Check the page output @@ -254,7 +254,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id) cea.save() - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Unenroll multiple students', 'multiple_students': 'student4_0@test.com, student2@test.com, student3@test.com', 'email_students': 'on'}) # Check the page output @@ -301,7 +301,7 @@ class TestInstructorEnrollsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) # Create activated, but not enrolled, user UserFactory.create(username="student5_0", email="student5_0@test.com", first_name="ShibTest", last_name="Enrolled") - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student5_0@test.com, student5_1@test.com', 'auto_enroll': 'on', 'email_students': 'on'}) # Check the page output diff --git a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py index 2da8d18d73..39003c0603 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py +++ b/lms/djangoapps/instructor/tests/test_legacy_forum_admin.py @@ -67,7 +67,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_forum_admin_users_for_unknown_user(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'unknown' for action in ['Add', 'Remove']: for rolename in FORUM_ROLES: @@ -76,7 +76,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_forum_admin_users_for_missing_roles(self): course = self.toy - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u1' for action in ['Add', 'Remove']: for rolename in FORUM_ROLES: @@ -86,7 +86,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_remove_forum_admin_users_for_missing_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u1' action = 'Remove' for rolename in FORUM_ROLES: @@ -96,7 +96,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_and_remove_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u2' for rolename in FORUM_ROLES: response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) @@ -109,7 +109,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_and_read_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u2' for rolename in FORUM_ROLES: # perform an add, and follow with a second identical add: @@ -121,7 +121,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_add_nonstaff_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u1' rolename = FORUM_ROLE_ADMINISTRATOR response = self.client.post(url, {'action': action_name('Add', rolename), FORUM_ADMIN_USER[rolename]: username}) @@ -130,7 +130,7 @@ class TestInstructorDashboardForumAdmin(ModuleStoreTestCase, LoginEnrollmentTest def test_list_forum_admin_users(self): course = self.toy self.initialize_roles(course.id) - url = reverse('instructor_dashboard', kwargs={'course_id': course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id}) username = 'u2' added_roles = [FORUM_ROLE_STUDENT] # u2 is already added as a student to the discussion forums self.assertTrue(has_forum_access(username, course.id, 'Student')) diff --git a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py index bca7528a96..75f5155d35 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_legacy_gradebook.py @@ -67,7 +67,7 @@ class TestGradebook(ModuleStoreTestCase): module_state_key=Location(item.location).url() ) - self.response = self.client.get(reverse('gradebook', args=(self.course.id,))) + self.response = self.client.get(reverse('gradebook_legacy', args=(self.course.id,))) def test_response_code(self): self.assertEquals(self.response.status_code, 200) diff --git a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py index cb7aa803d0..01f452f105 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py +++ b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py @@ -45,7 +45,7 @@ class TestRawGradeCSV(TestSubmittingProblems): resp = self.submit_question_answer('p2', {'2_1': 'Correct'}) self.assertEqual(resp.status_code, 200) - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id}) msg = "url = {0}\n".format(url) response = self.client.post(url, {'action': 'Download CSV of all RAW grades'}) msg += "instructor dashboard download raw csv grades: response = '{0}'\n".format(response) diff --git a/lms/djangoapps/instructor/tests/test_legacy_reset.py b/lms/djangoapps/instructor/tests/test_legacy_reset.py index ea259bc1fb..d3e9e1d351 100644 --- a/lms/djangoapps/instructor/tests/test_legacy_reset.py +++ b/lms/djangoapps/instructor/tests/test_legacy_reset.py @@ -53,7 +53,7 @@ class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCa sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash - url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id}) response = self.client.post(url, { 'action': 'Delete student state for module', 'unique_student_identifier': self.student.email, diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 479a246f02..f487e300f0 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1053,7 +1053,10 @@ def send_email(request, course_id): # Submit the task, so that the correct InstructorTask object gets created (for monitoring purposes) instructor_task.api.submit_bulk_course_email(request, course_id, email.id) # pylint: disable=E1101 - response_payload = {'course_id': course_id} + response_payload = { + 'course_id': course_id, + 'success': True, + } return JsonResponse(response_payload) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index ed9520420b..a7c7bee453 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _ from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from edxmako.shortcuts import render_to_response +from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.utils.html import escape from django.http import Http404 @@ -18,9 +19,10 @@ from xmodule.modulestore.django import modulestore from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from courseware.access import has_access -from courseware.courses import get_course_by_id, get_cms_course_link +from courseware.courses import get_course_by_id, get_cms_course_link, get_course_with_access from django_comment_client.utils import has_forum_access from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR +from instructor.offline_gradecalc import student_grades from student.models import CourseEnrollment from bulk_email.models import CourseAuthorization from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem @@ -79,7 +81,7 @@ def instructor_dashboard_2(request, course_id): context = { 'course': course, - 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), + 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': course_id}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, @@ -156,15 +158,23 @@ def _section_membership(course_id, access): def _section_student_admin(course_id, access): """ Provide data for the corresponding dashboard section """ + is_small_course = False + enrollment_count = CourseEnrollment.num_enrolled_in(course_id) + max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") + if max_enrollment_for_buttons is not None: + is_small_course = enrollment_count <= max_enrollment_for_buttons + section_data = { 'section_key': 'student_admin', 'section_display_name': _('Student Admin'), 'access': access, + 'is_small_course': is_small_course, 'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}), 'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}), 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}), 'rescore_problem_url': reverse('rescore_problem', kwargs={'course_id': course_id}), 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': course_id}), + 'spoc_gradebook_url': reverse('spoc_gradebook', kwargs={'course_id': course_id}), } return section_data @@ -246,3 +256,43 @@ def _section_metrics(course_id, access): 'get_students_problem_grades_url': reverse('get_students_problem_grades'), } return section_data + + +#---- Gradebook (shown to small courses only) ---- +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def spoc_gradebook(request, course_id): + """ + Show the gradebook for this course: + - Only shown for courses with enrollment < settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") + - Only displayed to course staff + """ + course = get_course_with_access(request.user, course_id, 'staff', depth=None) + + enrolled_students = User.objects.filter( + courseenrollment__course_id=course_id, + courseenrollment__is_active=1 + ).order_by('username').select_related("profile") + + # TODO (vshnayder): implement pagination to show to large courses + max_num_students = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") + enrolled_students = enrolled_students[:max_num_students] # HACK! + + student_info = [ + { + 'username': student.username, + 'id': student.id, + 'email': student.email, + 'grade_summary': student_grades(student, request, course), + 'realname': student.profile.name, + } + for student in enrolled_students + ] + + return render_to_response('courseware/gradebook.html', { + 'students': student_info, + 'course': course, + 'course_id': course_id, + # Checked above + 'staff_access': True, + 'ordered_grades': sorted(course.grade_cutoffs.items(), key=lambda i: i[1], reverse=True), + }) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 7f62501a12..1e8bad1bc4 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -1,6 +1,9 @@ """ Instructor Views """ +## NOTE: This is the code for the legacy instructor dashboard +## We are no longer supporting this file or accepting changes into it. + from contextlib import contextmanager import csv import json @@ -946,8 +949,7 @@ def instructor_dashboard(request, course_id): 'metrics_results': metrics_results, } - if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): - context['beta_dashboard_url'] = reverse('instructor_dashboard_2', kwargs={'course_id': course_id}) + context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_id}) return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index 884e5d874b..c0dbab529f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -169,8 +169,10 @@ FEATURES = { # Enable instructor to assign individual due dates 'INDIVIDUAL_DUE_DATES': False, - # Enable instructor dash beta version link - 'ENABLE_INSTRUCTOR_BETA_DASHBOARD': True, + # Enable legacy instructor dashboard + 'ENABLE_INSTRUCTOR_LEGACY_DASHBOARD': True, + # Is this an edX-owned domain? (used on instructor dashboard) + 'IS_EDX_DOMAIN': False, # Toggle to enable certificates of courses on dashboard 'ENABLE_VERIFIED_CERTIFICATES': False, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 4a2b9517e0..e1c68d92e4 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -31,11 +31,13 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms) FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True +FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'] = True FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True +FEATURES['IS_EDX_DOMAIN'] = True # Is this an edX-owned domain? (used on instructor dashboard) + FEEDBACK_SUBMISSION_EMAIL = "dummy@example.com" diff --git a/lms/envs/test.py b/lms/envs/test.py index edaa4c7c45..110e7a48f6 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -33,7 +33,7 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True +FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'] = True FEATURES['ENABLE_SHOPPING_CART'] = True diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee index 4459e407df..1235feaceb 100644 --- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee +++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee @@ -99,7 +99,7 @@ setup_instructor_dashboard = (idash_content) => e.preventDefault() # deactivate all link & section styles - idash_content.find(".#{CSS_INSTRUCTOR_NAV}").children().removeClass CSS_ACTIVE_SECTION + idash_content.find(".#{CSS_INSTRUCTOR_NAV} li").children().removeClass CSS_ACTIVE_SECTION idash_content.find(".#{CSS_IDASH_SECTION}").removeClass CSS_ACTIVE_SECTION # discover section paired to link diff --git a/lms/static/coffee/src/instructor_dashboard_tracking.coffee b/lms/static/coffee/src/instructor_dashboard_tracking.coffee index a4eab610c8..eaeb3a4862 100644 --- a/lms/static/coffee/src/instructor_dashboard_tracking.coffee +++ b/lms/static/coffee/src/instructor_dashboard_tracking.coffee @@ -1,4 +1,4 @@ if $('.instructor-dashboard-wrapper').length == 1 - analytics.track "Loaded an Instructor Dashboard Page", + analytics.track "Loaded a Legacy Instructor Dashboard Page", location: window.location.pathname dashboard_page: $('.navbar .selectedmode').text() diff --git a/lms/static/js/spec/staff_debug_actions_spec.js b/lms/static/js/spec/staff_debug_actions_spec.js index 347dd6d584..1a6a6a9691 100644 --- a/lms/static/js/spec/staff_debug_actions_spec.js +++ b/lms/static/js/spec/staff_debug_actions_spec.js @@ -6,7 +6,7 @@ describe('StaffDebugActions', function() { describe('get_url ', function() { it('defines url to courseware ajax entry point', function() { spyOn(StaffDebug, "get_current_url").andReturn("/courses/edX/Open_DemoX/edx_demo_course/courseware/stuff"); - expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor_dashboard/api/rescore_problem'); + expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor/api/rescore_problem'); }); }); @@ -40,7 +40,7 @@ describe('StaffDebugActions', function() { 'delete_module': false }); expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor_dashboard/api/reset_student_attempts' + '/instructor/api/reset_student_attempts' ); $('#' + fixture_id).remove(); }); @@ -59,7 +59,7 @@ describe('StaffDebugActions', function() { 'delete_module': true }); expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor_dashboard/api/reset_student_attempts' + '/instructor/api/reset_student_attempts' ); $('#' + fixture_id).remove(); @@ -79,7 +79,7 @@ describe('StaffDebugActions', function() { 'delete_module': false }); expect($.ajax.mostRecentCall.args[0]['url']).toEqual( - '/instructor_dashboard/api/rescore_problem' + '/instructor/api/rescore_problem' ); $('#' + fixture_id).remove(); }); diff --git a/lms/static/js/staff_debug_actions.js b/lms/static/js/staff_debug_actions.js index cf5bc85495..fb602a2388 100644 --- a/lms/static/js/staff_debug_actions.js +++ b/lms/static/js/staff_debug_actions.js @@ -7,7 +7,7 @@ var StaffDebug = (function(){ get_url = function(action){ var pathname = this.get_current_url(); - var url = pathname.substr(0,pathname.indexOf('/courseware')) + '/instructor_dashboard/api/' + action; + var url = pathname.substr(0,pathname.indexOf('/courseware')) + '/instructor/api/' + action; return url; } diff --git a/lms/static/sass/course/instructor/_instructor.scss b/lms/static/sass/course/instructor/_instructor.scss index 059db7a3ee..03211f7b45 100644 --- a/lms/static/sass/course/instructor/_instructor.scss +++ b/lms/static/sass/course/instructor/_instructor.scss @@ -121,9 +121,9 @@ } } } - + //Metrics tab - + .metrics-container { position: relative; width: 100%; @@ -170,6 +170,18 @@ border-radius: 5px; margin-top: 25px; } - + + .wrapper-msg { + margin-bottom: ($baseline*1.5); + + .msg { + margin-bottom: 0; + } + + .note { + margin: 0; + } + } + } diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 924de892c9..35a984c6e8 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -34,6 +34,11 @@ } // system feedback - messages + + .wrapper-msg { + margin-bottom: ($baseline*1.5); + } + .msg { border-radius: 1px; padding: $baseline/2 $baseline*0.75; @@ -43,6 +48,10 @@ .copy { font-weight: 600; } + + &.is-shown { + display: block; + } } // TYPE: warning @@ -51,6 +60,10 @@ background: tint($warning-color,95%); display: none; color: $warning-color; + + &.is-shown { + display: block; + } } // TYPE: confirm @@ -59,6 +72,10 @@ background: tint($confirm-color,95%); display: none; color: $confirm-color; + + &.is-shown { + display: block; + } } // TYPE: confirm @@ -69,6 +86,10 @@ .copy { color: $error-color; } + + &.is-shown { + display: block; + } } // inline copy @@ -110,6 +131,11 @@ section.instructor-dashboard-content-2 { // border: 1px solid blue; // } + .wrap-instructor-info { + display: inline; + top: 0; + } + .request-response-error { margin: 0; padding-bottom: ($baseline); @@ -141,8 +167,10 @@ section.instructor-dashboard-content-2 { h1 { @extend .top-header; + display: inline-block; padding-bottom: 0; border-bottom: 0; + margin-bottom: ($baseline*.75); } input[type="button"] { @@ -163,20 +191,29 @@ section.instructor-dashboard-content-2 { } .instructor-nav { - padding-bottom: 1em; + @extend %ui-no-list; + border-top: 1px solid $gray-l3; + border-bottom: 1px solid $gray-l3; - border-bottom: 1px solid #C8C8C8; - a { - margin-right: 1.2em; - } + .nav-item { + @extend %t-copy-base; + display: inline-block; + margin: ($baseline/2) $baseline; - .active-section { - color: #551A8B; + a { + display: block; + text-transform: uppercase; + + &.active-section { + color: $black; + } + } } } section.idash-section { display: none; + margin-top: ($baseline*1.5); // background-color: #0f0; &.active-section { @@ -560,14 +597,14 @@ section.instructor-dashboard-content-2 { float: left; clear: both; margin-top: 25px; - + .metrics-left { position: relative; width: 30%; height: 640px; float: left; margin-right: 2.5%; - + svg { width: 100%; } @@ -579,33 +616,33 @@ section.instructor-dashboard-content-2 { float: left; margin-left: 2.5%; margin-bottom: 25px; - + svg { width: 100%; } } - + svg { .stacked-bar { cursor: pointer; } } - + .metrics-tooltip { width: 250px; background-color: lightgray; padding: 3px; } - + .metrics-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: rgba(255,255,255, .75); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(255,255,255, .75); display: none; - + .metrics-overlay-content-wrapper { position: relative; display: block; @@ -616,23 +653,23 @@ section.instructor-dashboard-content-2 { border: 1px solid #000; border-radius: 25px; padding: 2.5%; - + .metrics-overlay-title { display: block; height: 50px; margin-bottom: 10px; font-weight: bold; } - + .metrics-overlay-content { width: 100%; height: 370px; overflow: auto; border: 1px solid #000; - + table { width: 100%; - + .header { background-color: #ddd; } @@ -641,18 +678,18 @@ section.instructor-dashboard-content-2 { } } } - + .overflow-message { padding-top: 20px; } - + .download-csv { position: absolute; display: none; right: 2%; bottom: 2%; } - + .close-button { position: absolute; right: 1.5%; @@ -661,27 +698,27 @@ section.instructor-dashboard-content-2 { } } } - + .stacked-bar-graph-legend { fill: white; } - + p.loading { padding-top: 100px; text-align: center; } - + p.nothing { padding-top: 25px; } - + h3.attention { padding: 10px; border: 1px solid #999; border-radius: 5px; margin-top: 25px; } - + input#graph_reload { display: none; } diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index f80a94644a..6ed4f90e02 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -1,10 +1,14 @@ +## NOTE: This is the template for the LEGACY instructor dashboard ## +## We are no longer supporting this file or accepting changes into it. ## +## Please see lms/templates/instructor for instructor dashboard templates ## + <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> <%namespace name='static' file='/static_content.html'/> -<%block name="pagetitle">${_("Instructor Dashboard")} +<%block name="pagetitle">${_("Legacy Instructor Dashboard")} <%block name="nav_skip">#instructor-dashboard-content <%block name="headextra"> @@ -122,13 +126,19 @@ function goto( mode) %if studio_url: ${_("View Course in Studio")} %endif - %if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): - ${_("Try New Beta Dashboard")} - %endif + ${_("Back To Instructor Dashboard")}
- +

${_("Instructor Dashboard")}

+ %if settings.FEATURES.get('IS_EDX_DOMAIN', False): + ## Only show this banner on the edx.org website (other sites may choose to show this if they wish) +
+

${_("You are using the legacy instructor dashboard, which we will retire in the near future.")} ${_("Return to the Instructor Dashboard")}

+

${_("If the Instructor Dashboard is missing functionality, please contact your PM to let us know.")}

+
+ %endif +

${_("Explanation of Roles:")}

${_("Forum Moderators: can edit or delete any post, remove misuse flags, close and re-open threads, endorse " @@ -686,7 +696,7 @@ function goto( mode)

- +

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

%for i in range(0,len(metrics_results['section_display_name'])): 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 680db26673..7d4cee28bc 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -53,35 +53,39 @@
-
-
- - -

${_("Instructor Dashboard")}

-
- ## links which are tied to idash-sections below. - ## the links are acativated and handled in instructor_dashboard.coffee - ## when the javascript loads, it clicks on the first section -

- % for section_data in sections: - ${_(section_data['section_display_name'])} - % endfor -

- - ## each section corresponds to a section_data sub-dictionary provided by the view - ## to keep this short, sections can be pulled out into their own files - - % for section_data in sections: -
- <%include file="${ section_data['section_key'] }.html" args="section_data=section_data" /> -
- % endfor - +
+
+
+ %if studio_url: + ${_("View Course in Studio")} + %endif + %if settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): + ${_("Revert to Legacy Dashboard")} + %endif +
+ +

${_("Instructor Dashboard")}

+
+

${_("We've changed the look and feel of the Instructor Dashboard. During this transition time, you can still access the old Instructor Dashboard by clicking the 'Revert to Legacy Dashboard' button above.")}

+
+ + ## links which are tied to idash-sections below. + ## the links are activated and handled in instructor_dashboard.coffee + ## when the javascript loads, it clicks on the first section + + + ## each section corresponds to a section_data sub-dictionary provided by the view + ## to keep this short, sections can be pulled out into their own files + + % for section_data in sections: +
+ <%include file="${ section_data['section_key'] }.html" args="section_data=section_data" />
-
+ % endfor +
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index 1ff3e2727c..93856d6695 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -1,6 +1,21 @@ <%! from django.utils.translation import ugettext as _ %> <%page args="section_data"/> +
+ %if section_data['is_small_course']: + ## Show the gradebook for small courses +

${_("Student Gradebook")}

+

+ ${_("Click here to view the gradebook for enrolled students. This feature is only visible to courses with a small number of total enrolled students.")} +

+
+

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

+
+ %endif +
+

${_("Student-specific grade inspection")}

diff --git a/lms/urls.py b/lms/urls.py index c063a49c2b..57513b72c5 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -270,14 +270,15 @@ if settings.COURSEWARE_ENABLED: # For the instructor url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor$', - 'instructor.views.legacy.instructor_dashboard', name="instructor_dashboard"), - - # see ENABLE_INSTRUCTOR_BETA_DASHBOARD section for more urls - + 'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor/api/', + include('instructor.views.api_urls')), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/gradebook$', - 'instructor.views.legacy.gradebook', name='gradebook'), - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/grade_summary$', - 'instructor.views.legacy.grade_summary', name='grade_summary'), + 'instructor.views.instructor_dashboard.spoc_gradebook', name='spoc_gradebook'), + + # see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls + + # Open Ended grading views url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading$', 'open_ended_grading.views.staff_grading', name='staff_grading'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_next$', @@ -364,13 +365,14 @@ if settings.COURSEWARE_ENABLED: ) -if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): +if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): urlpatterns += ( - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor_dashboard$', - 'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard_2"), - - url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/', - include('instructor.views.api_urls')) + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/legacy_gradebook$', + 'instructor.views.legacy.gradebook', name='gradebook_legacy'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/legacy_grade_summary$', + 'instructor.views.legacy.grade_summary', name='grade_summary_legacy'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/legacy_instructor_dash$', + 'instructor.views.legacy.instructor_dashboard', name="instructor_dashboard_legacy"), ) if settings.FEATURES.get('CLASS_DASHBOARD'):