From a9fc7aca69306b87c0325df3d5846ebb58eb60af Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Thu, 10 Dec 2015 15:49:48 -0500 Subject: [PATCH] Remove Legacy Instructor Dashboard --- .../management/commands/dump_grades.py | 76 - .../tests/test_legacy_enrollment.py | 357 ----- .../tests/test_legacy_raw_download_csv.py | 289 ---- .../instructor/tests/test_legacy_xss.py | 70 - .../instructor/views/instructor_dashboard.py | 2 - lms/djangoapps/instructor/views/legacy.py | 1237 ----------------- lms/envs/common.py | 3 - lms/envs/dev.py | 1 - lms/envs/test.py | 2 - lms/static/sass/_build-course.scss | 1 - .../sass/course/instructor/_instructor.scss | 189 --- .../legacy_instructor_dashboard.html | 496 ------- .../instructor_dashboard_2.html | 3 - lms/urls.py | 8 - 14 files changed, 2734 deletions(-) delete mode 100644 lms/djangoapps/instructor/management/commands/dump_grades.py delete mode 100644 lms/djangoapps/instructor/tests/test_legacy_enrollment.py delete mode 100644 lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py delete mode 100644 lms/djangoapps/instructor/tests/test_legacy_xss.py delete mode 100644 lms/djangoapps/instructor/views/legacy.py delete mode 100644 lms/static/sass/course/instructor/_instructor.scss delete mode 100644 lms/templates/courseware/legacy_instructor_dashboard.html diff --git a/lms/djangoapps/instructor/management/commands/dump_grades.py b/lms/djangoapps/instructor/management/commands/dump_grades.py deleted file mode 100644 index a0a1dcea18..0000000000 --- a/lms/djangoapps/instructor/management/commands/dump_grades.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/python -""" -django management command: dump grades to csv files -for use by batch processes -""" -import csv - -from instructor.views.legacy import get_student_grade_summary_data -from courseware.courses import get_course_by_id -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey -from opaque_keys.edx.locations import SlashSeparatedCourseKey - -from django.core.management.base import BaseCommand -from instructor.utils import DummyRequest - - -class Command(BaseCommand): - help = "dump grades to CSV file. Usage: dump_grades course_id_or_dir filename dump_type\n" - help += " course_id_or_dir: either course_id or course_dir\n" - help += " filename: where the output CSV is to be stored\n" - # help += " start_date: end date as M/D/Y H:M (defaults to end of available data)" - help += " dump_type: 'all' or 'raw' (see instructor dashboard)" - - def handle(self, *args, **options): - - # current grading logic and data schema doesn't handle dates - # datetime.strptime("21/11/06 16:30", "%m/%d/%y %H:%M") - - print "args = ", args - - course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' - fn = "grades.csv" - get_raw_scores = False - - if len(args) > 0: - course_id = args[0] - if len(args) > 1: - fn = args[1] - if len(args) > 2: - get_raw_scores = args[2].lower() == 'raw' - - request = DummyRequest() - # parse out the course into a coursekey - try: - course_key = CourseKey.from_string(course_id) - # if it's not a new-style course key, parse it from an old-style - # course key - except InvalidKeyError: - course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - - try: - course = get_course_by_id(course_key) - # Ok with catching general exception here because this is run as a management command - # and the exception is exposed right away to the user. - except Exception as err: # pylint: disable=broad-except - print "-----------------------------------------------------------------------------" - print "Sorry, cannot find course with id {}".format(course_id) - print "Got exception {}".format(err) - print "Please provide a course ID or course data directory name, eg content-mit-801rq" - return - - print "-----------------------------------------------------------------------------" - print "Dumping grades from {} to file {} (get_raw_scores={})".format(course.id, fn, get_raw_scores) - datatable = get_student_grade_summary_data(request, course, get_raw_scores=get_raw_scores) - - fp = open(fn, 'w') - - writer = csv.writer(fp, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) - writer.writerow([unicode(s).encode('utf-8') for s in datatable['header']]) - for datarow in datatable['data']: - encoded_row = [unicode(s).encode('utf-8') for s in datarow] - writer.writerow(encoded_row) - - fp.close() - print "Done: {} records dumped".format(len(datatable['data'])) diff --git a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py deleted file mode 100644 index e7ed19ae73..0000000000 --- a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py +++ /dev/null @@ -1,357 +0,0 @@ -""" -Unit tests for enrollment methods in views.py - -""" - -import ddt -from mock import patch -from nose.plugins.attrib import attr - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from courseware.tests.helpers import LoginEnrollmentTestCase -from xmodule.modulestore.tests.factories import CourseFactory -from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from student.models import CourseEnrollment, CourseEnrollmentAllowed -from instructor.views.legacy import get_and_clean_student_list, send_mail_to_student -from django.core import mail - -USER_COUNT = 4 - - -@attr('shard_1') -@ddt.ddt -class TestInstructorEnrollsStudent(SharedModuleStoreTestCase, LoginEnrollmentTestCase): - """ - Check Enrollment/Unenrollment with/without auto-enrollment on activation and with/without email notification - """ - @classmethod - def setUpClass(cls): - super(TestInstructorEnrollsStudent, cls).setUpClass() - cls.course = CourseFactory.create() - - def setUp(self): - super(TestInstructorEnrollsStudent, self).setUp() - - instructor = AdminFactory.create() - self.client.login(username=instructor.username, password='test') - - self.users = [ - UserFactory.create(username="student%d" % i, email="student%d@test.com" % i) - for i in xrange(USER_COUNT) - ] - - for user in self.users: - CourseEnrollmentFactory.create(user=user, course_id=self.course.id) - - # Empty the test outbox - mail.outbox = [] - - def test_unenrollment_email_off(self): - """ - Do un-enrollment email off test - """ - - course = self.course - - # Run the Un-enroll students command - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - response = self.client.post( - url, - { - 'action': 'Unenroll multiple students', - 'multiple_students': 'student0@test.com student1@test.com' - } - ) - - # Check the page output - self.assertContains(response, 'student0@test.com') - self.assertContains(response, 'student1@test.com') - self.assertContains(response, 'un-enrolled') - - # Check the enrollment table - user = User.objects.get(email='student0@test.com') - self.assertFalse(CourseEnrollment.is_enrolled(user, course.id)) - - user = User.objects.get(email='student1@test.com') - self.assertFalse(CourseEnrollment.is_enrolled(user, course.id)) - - # Check the outbox - self.assertEqual(len(mail.outbox), 0) - - def test_enrollment_new_student_autoenroll_on_email_off(self): - """ - Do auto-enroll on, email off test - """ - - course = self.course - - # Run the Enroll students command - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - 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 - self.assertContains(response, 'student1_1@test.com') - self.assertContains(response, 'student1_2@test.com') - self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment on') - - # Check the outbox - self.assertEqual(len(mail.outbox), 0) - - # Check the enrollmentallowed db entries - cea = CourseEnrollmentAllowed.objects.filter(email='student1_1@test.com', course_id=course.id) - self.assertEqual(1, cea[0].auto_enroll) - cea = CourseEnrollmentAllowed.objects.filter(email='student1_2@test.com', course_id=course.id) - self.assertEqual(1, cea[0].auto_enroll) - - # Check there is no enrollment db entry other than for the other students - ce = CourseEnrollment.objects.filter(course_id=course.id, is_active=1) - self.assertEqual(4, len(ce)) - - # Create and activate student accounts with same email - self.student1 = 'student1_1@test.com' - self.password = 'bar' - self.create_account('s1_1', self.student1, self.password) - self.activate_user(self.student1) - - self.student2 = 'student1_2@test.com' - self.create_account('s1_2', self.student2, self.password) - self.activate_user(self.student2) - - # Check students are enrolled - user = User.objects.get(email='student1_1@test.com') - self.assertTrue(CourseEnrollment.is_enrolled(user, course.id)) - - user = User.objects.get(email='student1_2@test.com') - self.assertTrue(CourseEnrollment.is_enrolled(user, course.id)) - - def test_repeat_enroll(self): - """ - Try to enroll an already enrolled student - """ - - course = self.course - - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - 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') - - def test_enrollmemt_new_student_autoenroll_off_email_off(self): - """ - Do auto-enroll off, email off test - """ - - course = self.course - - # Run the Enroll students command - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - response = self.client.post(url, {'action': 'Enroll multiple students', 'multiple_students': 'student2_1@test.com, student2_2@test.com'}) - - # Check the page output - self.assertContains(response, 'student2_1@test.com') - self.assertContains(response, 'student2_2@test.com') - self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment off') - - # Check the outbox - self.assertEqual(len(mail.outbox), 0) - - # Check the enrollmentallowed db entries - cea = CourseEnrollmentAllowed.objects.filter(email='student2_1@test.com', course_id=course.id) - self.assertEqual(0, cea[0].auto_enroll) - cea = CourseEnrollmentAllowed.objects.filter(email='student2_2@test.com', course_id=course.id) - self.assertEqual(0, cea[0].auto_enroll) - - # Check there is no enrollment db entry other than for the setup instructor and students - ce = CourseEnrollment.objects.filter(course_id=course.id, is_active=1) - self.assertEqual(4, len(ce)) - - # Create and activate student accounts with same email - self.student = 'student2_1@test.com' - self.password = 'bar' - self.create_account('s2_1', self.student, self.password) - self.activate_user(self.student) - - self.student = 'student2_2@test.com' - self.create_account('s2_2', self.student, self.password) - self.activate_user(self.student) - - # Check students are not enrolled - user = User.objects.get(email='student2_1@test.com') - self.assertFalse(CourseEnrollment.is_enrolled(user, course.id)) - - user = User.objects.get(email='student2_2@test.com') - self.assertFalse(CourseEnrollment.is_enrolled(user, course.id)) - - def test_get_and_clean_student_list(self): - """ - Clean user input test - """ - - string = "abc@test.com, def@test.com ghi@test.com \n \n jkl@test.com \n mno@test.com " - cleaned_string, _cleaned_string_lc = get_and_clean_student_list(string) - self.assertEqual(cleaned_string, ['abc@test.com', 'def@test.com', 'ghi@test.com', 'jkl@test.com', 'mno@test.com']) - - @ddt.data('http', 'https') - def test_enrollment_email_on(self, protocol): - """ - Do email on enroll test - """ - - course = self.course - - # Create activated, but not enrolled, user - UserFactory.create(username="student3_0", email="student3_0@test.com", first_name='Autoenrolled') - - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - params = {'action': 'Enroll multiple students', 'multiple_students': 'student3_0@test.com, student3_1@test.com, student3_2@test.com', 'auto_enroll': 'on', 'email_students': 'on'} - environ = {'wsgi.url_scheme': protocol} - response = self.client.post(url, params, **environ) - - # Check the page output - self.assertContains(response, 'student3_0@test.com') - self.assertContains(response, 'student3_1@test.com') - self.assertContains(response, 'student3_2@test.com') - self.assertContains(response, 'added, email sent') - self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment on, email sent') - - # Check the outbox - self.assertEqual(len(mail.outbox), 3) - self.assertEqual( - mail.outbox[0].subject, - 'You have been enrolled in {}'.format(course.display_name) - ) - self.assertEqual( - mail.outbox[0].body, - "Dear Autoenrolled Test\n\nYou have been enrolled in {} " - "at edx.org by a member of the course staff. " - "The course should now appear on your edx.org dashboard.\n\n" - "To start accessing course materials, please visit " - "{}://edx.org/courses/{}/\n\n" - "----\nThis email was automatically sent from edx.org to Autoenrolled Test".format( - course.display_name, protocol, unicode(course.id) - ) - ) - - self.assertEqual( - mail.outbox[1].subject, - 'You have been invited to register for {}'.format(course.display_name) - ) - self.assertEqual( - mail.outbox[1].body, - "Dear student,\n\nYou have been invited to join " - "{display_name} at edx.org by a member of the " - "course staff.\n\n" - "To finish your registration, please visit " - "{}://edx.org/register and fill out the registration form " - "making sure to use student3_1@test.com in the E-mail field.\n" - "Once you have registered and activated your account, you will " - "see {display_name} listed on your dashboard.\n\n" - "----\nThis email was automatically sent from edx.org to " - "student3_1@test.com".format(protocol, display_name=course.display_name) - ) - - def test_unenrollment_email_on(self): - """ - Do email on unenroll test - """ - - course = self.course - - # Create invited, but not registered, user - cea = CourseEnrollmentAllowed(email='student4_0@test.com', course_id=course.id) - cea.save() - - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - 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 - self.assertContains(response, 'student2@test.com') - self.assertContains(response, 'student3@test.com') - self.assertContains(response, 'un-enrolled, email sent') - - # Check the outbox - self.assertEqual(len(mail.outbox), 3) - self.assertEqual( - mail.outbox[0].subject, - 'You have been un-enrolled from {}'.format(course.display_name) - ) - self.assertEqual( - mail.outbox[0].body, - "Dear Student,\n\nYou have been un-enrolled from course " - "{} by a member of the course staff. " - "Please disregard the invitation previously sent.\n\n" - "----\nThis email was automatically sent from edx.org " - "to student4_0@test.com".format(course.display_name) - ) - self.assertEqual( - mail.outbox[1].subject, - 'You have been un-enrolled from {}'.format(course.display_name) - ) - - def test_send_mail_to_student(self): - """ - Do invalid mail template test - """ - - d = {'message': 'message_type_that_doesn\'t_exist'} - - send_mail_ret = send_mail_to_student('student0@test.com', d) - self.assertFalse(send_mail_ret) - - @ddt.data('http', 'https') - @patch('instructor.views.legacy.uses_shib') - def test_enrollment_email_on_shib_on(self, protocol, mock_uses_shib): - # Do email on enroll, shibboleth on test - - course = self.course - mock_uses_shib.return_value = True - - # 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_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - params = {'action': 'Enroll multiple students', 'multiple_students': 'student5_0@test.com, student5_1@test.com', 'auto_enroll': 'on', 'email_students': 'on'} - environ = {'wsgi.url_scheme': protocol} - response = self.client.post(url, params, **environ) - - # Check the page output - self.assertContains(response, 'student5_0@test.com') - self.assertContains(response, 'student5_1@test.com') - self.assertContains(response, 'added, email sent') - self.assertContains(response, 'user does not exist, enrollment allowed, pending with auto enrollment on, email sent') - - # Check the outbox - self.assertEqual(len(mail.outbox), 2) - self.assertEqual( - mail.outbox[0].subject, - 'You have been enrolled in {}'.format(course.display_name) - ) - self.assertEqual( - mail.outbox[0].body, - "Dear ShibTest Enrolled\n\nYou have been enrolled in {} " - "at edx.org by a member of the course staff. " - "The course should now appear on your edx.org dashboard.\n\n" - "To start accessing course materials, please visit " - "{}://edx.org/courses/{}/\n\n" - "----\nThis email was automatically sent from edx.org to ShibTest Enrolled".format( - course.display_name, protocol, unicode(course.id) - ) - ) - - self.assertEqual( - mail.outbox[1].subject, - 'You have been invited to register for {}'.format(course.display_name) - ) - self.assertEqual( - mail.outbox[1].body, - "Dear student,\n\nYou have been invited to join " - "{} at edx.org by a member of the " - "course staff.\n\n" - "To access the course visit {}://edx.org/courses/{}/ and login.\n\n" - "----\nThis email was automatically sent from edx.org to " - "student5_1@test.com".format( - course.display_name, protocol, course.id - ) - ) diff --git a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py b/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py deleted file mode 100644 index 24adeeec8a..0000000000 --- a/lms/djangoapps/instructor/tests/test_legacy_raw_download_csv.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Create course and answer a problem to test raw grade CSV -""" - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from instructor.utils import DummyRequest -from instructor.views.legacy import get_student_grade_summary_data -from nose.plugins.attrib import attr - -from courseware.tests.test_submitting_problems import TestSubmittingProblems -from student.roles import CourseStaffRole - - -@attr('shard_1') -class TestRawGradeCSV(TestSubmittingProblems): - """ - Tests around the instructor dashboard raw grade CSV - """ - - def setUp(self): - """ - Set up a simple course for testing basic grading functionality. - """ - super(TestRawGradeCSV, self).setUp() - - self.instructor = 'view2@test.com' - self.student_user2 = self.create_account('u2', self.instructor, self.password) - self.activate_user(self.instructor) - CourseStaffRole(self.course.id).add_users(User.objects.get(email=self.instructor)) - self.logout() - self.login(self.instructor, self.password) - self.enroll(self.course) - - # set up a simple course with four problems - self.homework = self.add_graded_section_to_course('homework', late=False, reset=False, showanswer=False) - self.add_dropdown_to_section(self.homework.location, 'p1', 1) - self.add_dropdown_to_section(self.homework.location, 'p2', 1) - self.add_dropdown_to_section(self.homework.location, 'p3', 1) - self.refresh_course() - - def answer_question(self): - """ - Answer a question correctly in the course - """ - self.login(self.instructor, self.password) - resp = self.submit_question_answer('p2', {'2_1': 'Correct'}) - self.assertEqual(resp.status_code, 200) - - def test_download_raw_grades_dump(self): - """ - Grab raw grade report and make sure all grades are reported. - """ - # Answer second problem correctly with 2nd user to expose bug - self.answer_question() - - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id.to_deprecated_string()}) - 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) - body = response.content.replace('\r', '') - msg += "body = '{0}'\n".format(body) - expected_csv = '''"ID","Username","Full Name","edX email","External email","p3","p2","p1" -"1","u1","username","view@test.com","","None","None","None" -"2","u2","username","view2@test.com","","0.0","1.0","0.0" -''' - self.assertEqual(body, expected_csv, msg) - - def get_expected_grade_data( - self, get_grades=True, get_raw_scores=False, - use_offline=False, get_score_max=False): - """ - Return expected results from the get_student_grade_summary_data call - with any options selected. - - Note that the kwargs accepted by get_expected_grade_data (and their - default values) must be identical to those in - get_student_grade_summary_data for this function to be accurate. - If kwargs are added or removed, or the functionality triggered by - them changes, this function must be updated to match. - - If get_score_max is True, instead of a single score between 0 and 1, - the actual score and total possible are returned. For example, if the - student got one out of two possible points, the values (1, 2) will be - returned instead of 0.5. - """ - expected_data = { - 'students': [self.student_user, self.student_user2], - 'header': [ - u'ID', u'Username', u'Full Name', u'edX email', u'External email', - u'HW 01', u'HW 02', u'HW 03', u'HW 04', u'HW 05', u'HW 06', u'HW 07', - u'HW 08', u'HW 09', u'HW 10', u'HW 11', u'HW 12', u'HW Avg', u'Lab 01', - u'Lab 02', u'Lab 03', u'Lab 04', u'Lab 05', u'Lab 06', u'Lab 07', - u'Lab 08', u'Lab 09', u'Lab 10', u'Lab 11', u'Lab 12', u'Lab Avg', u'Midterm', - u'Final' - ], - 'data': [ - [ - 1, u'u1', u'username', u'view@test.com', '', 0.0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ], - [ - 2, u'u2', u'username', u'view2@test.com', '', 0.3333333333333333, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0.03333333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0 - ] - ], - 'assignments': [ - u'HW 01', u'HW 02', u'HW 03', u'HW 04', u'HW 05', u'HW 06', u'HW 07', u'HW 08', - u'HW 09', u'HW 10', u'HW 11', u'HW 12', u'HW Avg', u'Lab 01', u'Lab 02', - u'Lab 03', u'Lab 04', u'Lab 05', u'Lab 06', u'Lab 07', u'Lab 08', u'Lab 09', - u'Lab 10', u'Lab 11', u'Lab 12', u'Lab Avg', u'Midterm', u'Final' - ] - } - - # The first five columns contain the student ID, username, - # full name, and e-mail addresses. - non_grade_columns = 5 - # If the following 'if' is triggered, the - # get_student_grade_summary_data function will not return any - # grade data. Only the "non_grade_columns." - # So strip out the headers beyond the "non_grade_columns," and - # strip out all the grades in the 'data' key. - if not get_grades or use_offline: - expected_data["header"] = expected_data["header"][:non_grade_columns] - # This iterates over the lists of grades in the 'data' key - # of the return dictionary and strips out everything after - # the non_grade_columns. - for index, rec in enumerate(expected_data["data"]): - expected_data["data"][index] = rec[:non_grade_columns] - # Wipe out all data in the 'assignments' key if use_offline - # is True; no assignment data is returned. - if use_offline: - expected_data['assignments'] = [] - # If get_grades is False, get_student_grade_summary_data doesn't - # even return an 'assignments' key, so delete it. - if get_grades is False: - del expected_data['assignments'] - # If get_raw_scores is true, get_student_grade_summary_data returns - # the raw score per assignment. For example, the "0.3333333333333333" - # in the data above is for getting one out of three possible - # answers correct. Getting raw scores means the actual score (1) is - # return instead of: 1.0/3.0 - # For some reason, this also causes it to not to return any assignments - # without attempts, so most of the headers are removed. - elif get_raw_scores: - expected_data["data"] = [ - [ - 1, u'u1', u'username', u'view@test.com', - '', None, None, None - ], - [ - 2, u'u2', u'username', u'view2@test.com', - '', 0.0, 1.0, 0.0 - ], - ] - expected_data["assignments"] = [u'p3', u'p2', u'p1'] - expected_data["header"] = [ - u'ID', u'Username', u'Full Name', u'edX email', - u'External email', u'p3', u'p2', u'p1' - ] - # Strip out the single-value float scores and replace them - # with two-tuples of actual and possible scores (see docstring). - if get_score_max: - expected_data["data"][-1][-3:] = (0.0, 1), (1.0, 1.0), (0.0, 1) - - return expected_data - - def test_grade_summary_data_defaults(self): - """ - Test grade summary data report generation with all default kwargs. - - This test compares the output of the get_student_grade_summary_data - with a dictionary of exected values. The purpose of this test is - to ensure that future changes to the get_student_grade_summary_data - function (for example, mitocw/edx-platform #95). - """ - request = DummyRequest() - self.answer_question() - data = get_student_grade_summary_data(request, self.course) - expected_data = self.get_expected_grade_data() - self.compare_data(data, expected_data) - - def test_grade_summary_data_raw_scores(self): - """ - Test grade summary data report generation with get_raw_scores True. - """ - request = DummyRequest() - self.answer_question() - data = get_student_grade_summary_data( - request, self.course, get_raw_scores=True, - ) - expected_data = self.get_expected_grade_data(get_raw_scores=True) - self.compare_data(data, expected_data) - - def test_grade_summary_data_no_grades(self): - """ - Test grade summary data report generation with - get_grades set to False. - """ - request = DummyRequest() - self.answer_question() - - data = get_student_grade_summary_data( - request, self.course, get_grades=False - ) - expected_data = self.get_expected_grade_data(get_grades=False) - # if get_grades is False, get_expected_grade_data does not - # add an "assignments" key. - self.assertNotIn("assignments", expected_data) - self.compare_data(data, expected_data) - - def test_grade_summary_data_use_offline(self): - """ - Test grade summary data report generation with use_offline True. - """ - request = DummyRequest() - self.answer_question() - data = get_student_grade_summary_data( - request, self.course, use_offline=True) - expected_data = self.get_expected_grade_data(use_offline=True) - self.compare_data(data, expected_data) - - def test_grade_summary_data_use_offline_and_raw_scores(self): - """ - Test grade summary data report generation with use_offline - and get_raw_scores both True. - """ - request = DummyRequest() - self.answer_question() - data = get_student_grade_summary_data( - request, self.course, use_offline=True, get_raw_scores=True - ) - expected_data = self.get_expected_grade_data( - use_offline=True, get_raw_scores=True - ) - self.compare_data(data, expected_data) - - def test_grade_summary_data_get_score_max(self): - """ - Test grade summary data report generation with get_score_max set - to True (also requires get_raw_scores to be True). - """ - request = DummyRequest() - self.answer_question() - data = get_student_grade_summary_data( - request, self.course, use_offline=True, get_raw_scores=True, - get_score_max=True, - ) - expected_data = self.get_expected_grade_data( - use_offline=True, get_raw_scores=True, get_score_max=True, - ) - self.compare_data(data, expected_data) - - def compare_data(self, data, expected_data): - """ - Compare the output of the get_student_grade_summary_data - function to the expected_data data. - """ - - # Currently, all kwargs to get_student_grade_summary_data - # return a dictionary with the same keys, except for - # get_grades=False, which results in no 'assignments' key. - # This is explicitly checked for above in - # test_grade_summary_data_no_grades. This is a backup which - # will catch future changes. - self.assertListEqual( - expected_data.keys(), - data.keys(), - ) - - # Ensure the student info and assignment names are as expected. - for key in ['assignments', 'header']: - self.assertListEqual( - expected_data.get(key, []), - data.get(key, []), - ) - - # Ensure each student's grades are as expected for each assignment. - for index, student in enumerate(expected_data['students']): - self.assertEqual( - student.username, - data['students'][index].username - ) - self.assertListEqual( - expected_data['data'][index], - data['data'][index] - ) diff --git a/lms/djangoapps/instructor/tests/test_legacy_xss.py b/lms/djangoapps/instructor/tests/test_legacy_xss.py deleted file mode 100644 index 919c512dcd..0000000000 --- a/lms/djangoapps/instructor/tests/test_legacy_xss.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Tests of various instructor dashboard features that include lists of students -""" - -from django.conf import settings -from django.test.client import RequestFactory -from markupsafe import escape -from nose.plugins.attrib import attr - -from student.tests.factories import UserFactory, CourseEnrollmentFactory -from edxmako.tests import mako_middleware_process_request -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory - -from instructor.views import legacy - -# pylint: disable=missing-docstring - - -@attr('shard_1') -class TestXss(SharedModuleStoreTestCase): - @classmethod - def setUpClass(cls): - super(TestXss, cls).setUpClass() - cls._course = CourseFactory.create() - - def setUp(self): - super(TestXss, self).setUp() - - self._request_factory = RequestFactory() - self._evil_student = UserFactory.create( - email="robot+evil@edx.org", - username="evil-robot", - profile__name='Evil Robot', - ) - self._instructor = UserFactory.create( - email="robot+instructor@edx.org", - username="instructor", - is_staff=True - ) - CourseEnrollmentFactory.create( - user=self._evil_student, - course_id=self._course.id - ) - - def _test_action(self, action): - """ - Test for XSS vulnerability in the given action - - Build a request with the given action, call the instructor dashboard - view, and check that HTML code in a user's name is properly escaped. - """ - req = self._request_factory.post( - "dummy_url", - data={"action": action} - ) - req.user = self._instructor - req.session = {} - - mako_middleware_process_request(req) - resp = legacy.instructor_dashboard(req, self._course.id.to_deprecated_string()) - respUnicode = resp.content.decode(settings.DEFAULT_CHARSET) - self.assertNotIn(self._evil_student.profile.name, respUnicode) - self.assertIn(escape(self._evil_student.profile.name), respUnicode) - - def test_list_enrolled(self): - self._test_action("List enrolled students") - - def test_dump_list_of_enrolled(self): - self._test_action("Dump list of enrolled students") diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index dba499e1c2..c3d8c00b42 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -195,8 +195,6 @@ def instructor_dashboard_2(request, course_id): 'generate_bulk_certificate_exceptions_url': generate_bulk_certificate_exceptions_url, 'certificate_exception_view_url': certificate_exception_view_url } - if settings.FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD']: - context['old_dashboard_url'] = reverse('instructor_dashboard_legacy', kwargs={'course_id': unicode(course_key)}) return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py deleted file mode 100644 index 3d12f49759..0000000000 --- a/lms/djangoapps/instructor/views/legacy.py +++ /dev/null @@ -1,1237 +0,0 @@ -""" -Instructor Views -""" -## NOTE: This is the code for the legacy instructor dashboard -## We are no longer supporting this file or accepting changes into it. -# pylint: disable=line-too-long, missing-docstring -from contextlib import contextmanager -import csv -import json -import logging -import os -import re -import requests -import urllib - -from collections import defaultdict, OrderedDict -from markupsafe import escape -from requests.status_codes import codes -from StringIO import StringIO - -from django.conf import settings -from django.contrib.auth.models import User -from django.db import transaction -from django.http import HttpResponse -from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.decorators.cache import cache_control -from django.core.urlresolvers import reverse -from django.core.mail import send_mail -from django.utils import timezone - -import xmodule.graders as xmgraders -from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import modulestore -from opaque_keys.edx.locations import SlashSeparatedCourseKey - -from courseware import grades -from courseware.access import has_access -from courseware.courses import get_course_with_access, get_cms_course_link -from courseware.models import StudentModule -from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR -from django_comment_client.utils import has_forum_access -from instructor.offline_gradecalc import student_grades, offline_grades_available -from instructor.views.tools import strip_if_string, bulk_email_is_enabled_for_course, add_block_ids -from instructor_task.api import ( - get_running_instructor_tasks, - get_instructor_task_history, -) -from instructor_task.views import get_task_completion_info -from edxmako.shortcuts import render_to_response, render_to_string -from class_dashboard import dashboard_data -from student.models import ( - CourseEnrollment, - CourseEnrollmentAllowed, -) -import track.views -from django.utils.translation import ugettext as _ - -from microsite_configuration import microsite -from opaque_keys.edx.locations import i4xEncoder -from openedx.core.djangoapps.course_groups.cohorts import is_course_cohorted - - -log = logging.getLogger(__name__) - -# internal commands for managing forum roles: -FORUM_ROLE_ADD = 'add' -FORUM_ROLE_REMOVE = 'remove' - -# For determining if a shibboleth course -SHIBBOLETH_DOMAIN_PREFIX = 'shib:' - - -def split_by_comma_and_whitespace(a_str): - """ - Return string a_str, split by , or whitespace - """ - return re.split(r'[\s,]', a_str) - - -# Grades can potentially be written - if so, let grading manage the transaction. -@transaction.non_atomic_requests -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -def instructor_dashboard(request, course_id): - """Display the instructor dashboard for a course.""" - course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - course = get_course_with_access(request.user, 'staff', course_key, depth=None) - - instructor_access = bool(has_access(request.user, 'instructor', course)) # an instructor can manage staff lists - - forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) - - msg = '' - show_email_tab = False - problems = [] - plots = [] - datatable = {} - - # the instructor dashboard page is modal: grades, admin - # keep that state in request.session (defaults to grades mode) - idash_mode = request.POST.get('idash_mode', '') - idash_mode_key = u'idash_mode:{0}'.format(course_id) - if idash_mode: - request.session[idash_mode_key] = idash_mode - else: - idash_mode = request.session.get(idash_mode_key, 'Grades') - - enrollment_number = CourseEnrollment.objects.num_enrolled_in(course_key) - - # assemble some course statistics for output to instructor - def get_course_stats_table(): - datatable = { - 'header': ['Statistic', 'Value'], - 'title': _('Course Statistics At A Glance'), - } - - data = [['Date', timezone.now().isoformat()]] - data += compute_course_stats(course).items() - if request.user.is_staff: - for field in course.fields.values(): - if getattr(field.scope, 'user', False): - continue - - data.append([ - field.name, - json.dumps(field.read_json(course), cls=i4xEncoder) - ]) - datatable['data'] = data - return datatable - - def return_csv(func, datatable, file_pointer=None): - """Outputs a CSV file from the contents of a datatable.""" - if file_pointer is None: - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') - else: - response = file_pointer - writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) - encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] - writer.writerow(encoded_row) - for datarow in datatable['data']: - # 's' here may be an integer, float (eg score) or string (eg student name) - encoded_row = [ - # If s is already a UTF-8 string, trying to make a unicode - # object out of it will fail unless we pass in an encoding to - # the constructor. But we can't do that across the board, - # because s is often a numeric type. So just do this. - s if isinstance(s, str) else unicode(s).encode('utf-8') - for s in datarow - ] - writer.writerow(encoded_row) - return response - - # process actions from form POST - action = request.POST.get('action', '') - use_offline = request.POST.get('use_offline_grades', False) - - if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: - if 'GIT pull' in action: - data_dir = course.data_dir - log.debug('git pull %s', data_dir) - gdir = settings.DATA_DIR / data_dir - if not os.path.exists(gdir): - msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) - else: - cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) - msg += "git pull on {0}:

".format(data_dir) - msg += "

{0}

".format(escape(os.popen(cmd).read())) - track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard") - - if 'Reload course' in action: - log.debug('reloading %s (%s)', course_key, course) - try: - data_dir = course.data_dir - modulestore().try_load_course(data_dir) - msg += "

Course reloaded from {0}

".format(data_dir) - track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") - course_errors = modulestore().get_course_errors(course.id) - msg += '' - except Exception as err: # pylint: disable=broad-except - msg += '

Error: {0}

'.format(escape(err)) - - if action == 'Dump list of enrolled students' or action == 'List enrolled students': - log.debug(action) - datatable = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) - datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) - track.views.server_track(request, "list-students", {}, page="idashboard") - - elif 'Dump all RAW grades' in action: - log.debug(action) - datatable = get_student_grade_summary_data(request, course, get_grades=True, - get_raw_scores=True, use_offline=use_offline) - datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key) - track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") - - elif 'Download CSV of all RAW grades' in action: - track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") - return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), - get_student_grade_summary_data(request, course, get_raw_scores=True, use_offline=use_offline)) - - elif 'Download CSV of answer distributions' in action: - track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") - return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key)) - - #---------------------------------------- - # export grades to remote gradebook - - elif action == 'List assignments available in remote gradebook': - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') - msg += msg2 - - elif action == 'List assignments available for this course': - log.debug(action) - allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) - - assignments = [[x] for x in allgrades['assignments']] - datatable = {'header': [_('Assignment Name')]} - datatable['data'] = assignments - datatable['title'] = action - - msg += 'assignments=
%s
' % assignments - - elif action == 'List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) - msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') - datatable = {'header': ['Student email', 'Match?']} - rg_students = [x['email'] for x in rg_stud_data['retdata']] - - def domatch(student): - """Returns 'yes' if student is pressent in the remote gradebook student list, else returns 'No'""" - return 'yes' if student.email in rg_students else 'No' - datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] - datatable['title'] = action - - elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', - 'Export CSV file of grades for assignment']: - - log.debug(action) - datatable = {} - aname = request.POST.get('assignment_name', '') - if not aname: - msg += "{text}".format(text=_("Please enter an assignment name")) - else: - allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) - if aname not in allgrades['assignments']: - msg += "{text}".format( - text=_("Invalid assignment name '{name}'").format(name=aname) - ) - else: - aidx = allgrades['assignments'].index(aname) - datatable = {'header': [_('External email'), aname]} - ddata = [] - for student in allgrades['students']: # do one by one in case there is a student who has only partial grades - try: - ddata.append([student.email, student.grades[aidx]]) - except IndexError: - log.debug(u'No grade for assignment %(idx)s (%(name)s) for student %(email)s', { - "idx": aidx, - "name": aname, - "email": student.email, - }) - datatable['data'] = ddata - - datatable['title'] = _('Grades for assignment "{name}"').format(name=aname) - - if 'Export CSV' in action: - # generate and return CSV file - return return_csv('grades {name}.csv'.format(name=aname), datatable) - - elif 'remote gradebook' in action: - file_pointer = StringIO() - return_csv('', datatable, file_pointer=file_pointer) - file_pointer.seek(0) - files = {'datafile': file_pointer} - msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) - msg += msg2 - - #---------------------------------------- - # enrollment - - elif action == 'Enroll multiple students': - - is_shib_course = uses_shib(course) - students = request.POST.get('multiple_students', '') - auto_enroll = bool(request.POST.get('auto_enroll')) - email_students = bool(request.POST.get('email_students')) - secure = request.is_secure() - ret = _do_enroll_students(course, course_key, students, secure=secure, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) - datatable = ret['datatable'] - - elif action == 'Unenroll multiple students': - - students = request.POST.get('multiple_students', '') - email_students = bool(request.POST.get('email_students')) - ret = _do_unenroll_students(course_key, students, email_students=email_students) - datatable = ret['datatable'] - - elif action == 'List sections available in remote gradebook': - - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') - msg += msg2 - - elif action in ['List students in section in remote gradebook', - 'Overload enrollment list using remote gradebook', - 'Merge enrollment list with remote gradebook']: - - section = request.POST.get('gradebook_section', '') - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section)) - msg += msg2 - - if 'List' not in action: - students = ','.join([x['email'] for x in datatable['retdata']]) - overload = 'Overload' in action - secure = request.is_secure() - ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload) - datatable = ret['datatable'] - - #---------------------------------------- - # analytics - def get_analytics_result(analytics_name): - """Return data for an Analytic piece, or None if it doesn't exist. It - logs and swallows errors. - """ - url = settings.ANALYTICS_SERVER_URL + \ - u"get?aname={}&course_id={}&apikey={}".format( - analytics_name, urllib.quote(unicode(course_key)), settings.ANALYTICS_API_KEY - ) - try: - res = requests.get(url) - except Exception: # pylint: disable=broad-except - log.exception("Error trying to access analytics at %s", url) - return None - - if res.status_code == codes.OK: - # WARNING: do not use req.json because the preloaded json doesn't - # preserve the order of the original record (hence OrderedDict). - payload = json.loads(res.content, object_pairs_hook=OrderedDict) - add_block_ids(payload) - return payload - else: - log.error("Error fetching %s, code: %s, msg: %s", - url, res.status_code, res.content) - return None - - analytics_results = {} - - if idash_mode == 'Analytics': - dashboard_analytics = [ - # "StudentsAttemptedProblems", # num students who tried given problem - "StudentsDailyActivity", # active students by day - "StudentsDropoffPerDay", # active students dropoff by day - # "OverallGradeDistribution", # overall point distribution for course - # "StudentsPerProblemCorrect", # foreach problem, num students correct - "ProblemGradeDistribution", # foreach problem, grade distribution - ] - - for analytic_name in dashboard_analytics: - analytics_results[analytic_name] = get_analytics_result(analytic_name) - - #---------------------------------------- - # Metrics - - metrics_results = {} - if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': - metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key) - metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key) - - #---------------------------------------- - # offline grades? - - if use_offline: - msg += "
{text}".format( - text=_("Grades from {course_id}").format( - course_id=offline_grades_available(course_key) - ) - ) - - # generate list of pending background tasks - if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): - instructor_tasks = get_running_instructor_tasks(course_key) - else: - instructor_tasks = None - - # determine if this is a studio-backed course so we can provide a link to edit this course in studio - is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml - studio_url = None - if is_studio_course: - studio_url = get_cms_course_link(course) - - if bulk_email_is_enabled_for_course(course_key): - show_email_tab = True - - # display course stats only if there is no other table to display: - course_stats = None - if not datatable: - course_stats = get_course_stats_table() - - # disable buttons for large courses - disable_buttons = False - max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") - if max_enrollment_for_buttons is not None: - disable_buttons = enrollment_number > max_enrollment_for_buttons - - #---------------------------------------- - # context for rendering - - context = { - 'course': course, - 'course_is_cohorted': is_course_cohorted(course.id), - 'staff_access': True, - 'admin_access': request.user.is_staff, - 'instructor_access': instructor_access, - 'forum_admin_access': forum_admin_access, - 'datatable': datatable, - 'course_stats': course_stats, - 'msg': msg, - 'modeflag': {idash_mode: 'selectedmode'}, - 'studio_url': studio_url, - - 'show_email_tab': show_email_tab, # email - - 'course_errors': modulestore().get_course_errors(course.id), - 'instructor_tasks': instructor_tasks, - 'offline_grade_log': offline_grades_available(course_key), - - 'analytics_results': analytics_results, - 'disable_buttons': disable_buttons, - 'metrics_results': metrics_results, - } - - context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}) - - return render_to_response('courseware/legacy_instructor_dashboard.html', context) - - -def _do_remote_gradebook(user, course, action, args=None, files=None): - ''' - Perform remote gradebook action. Returns msg, datatable. - ''' - rgb = course.remote_gradebook - if not rgb: - msg = _("No remote gradebook defined in course metadata") - return msg, {} - - rgburl = settings.FEATURES.get('REMOTE_GRADEBOOK_URL', '') - if not rgburl: - msg = _("No remote gradebook url defined in settings.FEATURES") - return msg, {} - - rgbname = rgb.get('name', '') - if not rgbname: - msg = _("No gradebook name defined in course remote_gradebook metadata") - return msg, {} - - if args is None: - args = {} - data = dict(submit=action, gradebook=rgbname, user=user.email) - data.update(args) - - try: - resp = requests.post(rgburl, data=data, verify=False, files=files) - retdict = json.loads(resp.content) - except Exception as err: # pylint: disable=broad-except - msg = _("Failed to communicate with gradebook server at {url}").format(url=rgburl) + "
" - msg += _("Error: {err}").format(err=err) - msg += "
resp={resp}".format(resp=resp.content) - msg += "
data={data}".format(data=data) - return msg, {} - - msg = '
{msg}
'.format(msg=retdict['msg'].replace('\n', '
')) - retdata = retdict['data'] # a list of dicts - - if retdata: - datatable = {'header': retdata[0].keys()} - datatable['data'] = [x.values() for x in retdata] - datatable['title'] = _('Remote gradebook response for {action}').format(action=action) - datatable['retdata'] = retdata - else: - datatable = {} - - return msg, datatable - - -def _role_members_table(role, title, course_key): - """ - Return a data table of usernames and names of users in group_name. - - Arguments: - role -- a student.roles.AccessRole - title -- a descriptive title to show the user - - Returns: - a dictionary with keys - 'header': ['Username', 'Full name'], - 'data': [[username, name] for all users] - 'title': "{title} in course {course}" - """ - uset = role.users_with_role() - datatable = {'header': [_('Username'), _('Full name')]} - datatable['data'] = [[x.username, x.profile.name] for x in uset] - datatable['title'] = _('{title} in course {course_key}').format(title=title, course_key=course_key.to_deprecated_string()) - return datatable - - -def _user_from_name_or_email(username_or_email): - """ - Return the `django.contrib.auth.User` with the supplied username or email. - - If `username_or_email` contains an `@` it is treated as an email, otherwise - it is treated as the username - """ - username_or_email = strip_if_string(username_or_email) - - if '@' in username_or_email: - return User.objects.get(email=username_or_email) - else: - return User.objects.get(username=username_or_email) - - -def add_user_to_role(request, username_or_email, role, group_title, event_name): - """ - Look up the given user by username (if no '@') or email (otherwise), and add them to group. - - Arguments: - request: django request--used for tracking log - username_or_email: who to add. Decide if it's an email by presense of an '@' - group: A group name - group_title: what to call this group in messages to user--e.g. "beta-testers". - event_name: what to call this event when logging to tracking logs. - - Returns: - html to insert in the message field - """ - username_or_email = strip_if_string(username_or_email) - try: - user = _user_from_name_or_email(username_or_email) - except User.DoesNotExist: - return u'Error: unknown username or email "{0}"'.format(username_or_email) - - role.add_users(user) - - # Deal with historical event names - if event_name in ('staff', 'beta-tester'): - track.views.server_track( - request, - "add-or-remove-user-group", - { - "event_name": event_name, - "user": unicode(user), - "event": "add" - }, - page="idashboard" - ) - else: - track.views.server_track(request, "add-instructor", {"instructor": unicode(user)}, page="idashboard") - - return 'Added {0} to {1}'.format(user, group_title) - - -def remove_user_from_role(request, username_or_email, role, group_title, event_name): - """ - Look up the given user by username (if no '@') or email (otherwise), and remove them from the supplied role. - - Arguments: - request: django request--used for tracking log - username_or_email: who to remove. Decide if it's an email by presense of an '@' - role: A student.roles.AccessRole - group_title: what to call this group in messages to user--e.g. "beta-testers". - event_name: what to call this event when logging to tracking logs. - - Returns: - html to insert in the message field - """ - - username_or_email = strip_if_string(username_or_email) - try: - user = _user_from_name_or_email(username_or_email) - except User.DoesNotExist: - return u'Error: unknown username or email "{0}"'.format(username_or_email) - - role.remove_users(user) - - # Deal with historical event names - if event_name in ('staff', 'beta-tester'): - track.views.server_track( - request, - "add-or-remove-user-group", - { - "event_name": event_name, - "user": unicode(user), - "event": "remove" - }, - page="idashboard" - ) - else: - track.views.server_track(request, "remove-instructor", {"instructor": unicode(user)}, page="idashboard") - - return 'Removed {0} from {1}'.format(user, group_title) - - -class GradeTable(object): - """ - Keep track of grades, by student, for all graded assignment - components. Each student's grades are stored in a list. The - index of this list specifies the assignment component. Not - all lists have the same length, because at the start of going - through the set of grades, it is unknown what assignment - compoments exist. This is because some students may not do - all the assignment components. - - The student grades are then stored in a dict, with the student - id as the key. - """ - def __init__(self): - self.components = OrderedDict() - self.grades = {} - self._current_row = {} - - def _add_grade_to_row(self, component, score, possible=None): - """Creates component if needed, and assigns score - - Args: - component (str): Course component being graded - score (float): Score of student on component - possible (float): Max possible score for the component - - Returns: - None - """ - component_index = self.components.setdefault(component, len(self.components)) - if possible is not None: - # send a tuple instead of a single value - score = (score, possible) - self._current_row[component_index] = score - - @contextmanager - def add_row(self, student_id): - """Context management for a row of grades - - Uses a new dictionary to get all grades of a specified student - and closes by adding that dict to the internal table. - - Args: - student_id (str): Student id that is having grades set - - """ - self._current_row = {} - yield self._add_grade_to_row - self.grades[student_id] = self._current_row - - def get_grade(self, student_id): - """Retrieves padded list of grades for specified student - - Args: - student_id (str): Student ID for desired grades - - Returns: - list: Ordered list of grades for student - - """ - row = self.grades.get(student_id, []) - ncomp = len(self.components) - return [row.get(comp, None) for comp in range(ncomp)] - - def get_graded_components(self): - """ - Return a list of components that have been - discovered so far. - """ - return self.components.keys() - - -def get_student_grade_summary_data( - request, course, get_grades=True, get_raw_scores=False, - use_offline=False, get_score_max=False -): - """ - Return data arrays with student identity and grades for specified course. - - course = CourseDescriptor - course_key = course ID - - Note: both are passed in, only because instructor_dashboard already has them already. - - returns datatable = dict(header=header, data=data) - where - - header = list of strings labeling the data fields - data = list (one per student) of lists of data corresponding to the fields - - If get_raw_scores=True, then instead of grade summaries, the raw grades for all graded modules are returned. - - If get_score_max is True, two values will be returned for each grade -- the - total number of points earned and the total number of points possible. For - example, if two points are possible and one is earned, (1, 2) will be - returned instead of 0.5 (the default). - """ - course_key = course.id - enrolled_students = User.objects.filter( - courseenrollment__course_id=course_key, - courseenrollment__is_active=1, - ).prefetch_related("groups").order_by('username') - - header = [_('ID'), _('Username'), _('Full Name'), _('edX email'), _('External email')] - - datatable = {'header': header, 'students': enrolled_students} - data = [] - - gtab = GradeTable() - - for student in enrolled_students: - datarow = [student.id, student.username, student.profile.name, student.email] - try: - datarow.append(student.externalauthmap.external_email) - except Exception: # pylint: disable=broad-except - datarow.append('') - - if get_grades: - gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) - log.debug(u'student=%s, gradeset=%s', student, gradeset) - with gtab.add_row(student.id) as add_grade: - if get_raw_scores: - # The following code calls add_grade, which is an alias - # for the add_row method on the GradeTable class. This adds - # a grade for each assignment. Depending on whether - # get_score_max is True, it will return either a single - # value as a float between 0 and 1, or a two-tuple - # containing the earned score and possible score for - # the assignment (see docstring). - for score in gradeset['raw_scores']: - if get_score_max is True: - add_grade(score.section, score.earned, score.possible) - else: - add_grade(score.section, score.earned) - else: - for grade_item in gradeset['section_breakdown']: - add_grade(grade_item['label'], grade_item['percent']) - student.grades = gtab.get_grade(student.id) - - data.append(datarow) - - # if getting grades, need to do a second pass, and add grades to each datarow; - # on the first pass we don't know all the graded components - if get_grades: - for datarow in data: - # get grades for student - sgrades = gtab.get_grade(datarow[0]) - datarow += sgrades - - # get graded components and add to table header - assignments = gtab.get_graded_components() - header += assignments - datatable['assignments'] = assignments - - datatable['data'] = data - return datatable - -#----------------------------------------------------------------------------- - -# Gradebook has moved to instructor.api.spoc_gradebook # - -#----------------------------------------------------------------------------- -# enrollment - - -def _do_enroll_students(course, course_key, students, secure=False, overload=False, auto_enroll=False, email_students=False, is_shib_course=False): - """ - Do the actual work of enrolling multiple students, presented as a string - of emails separated by commas or returns - `course` is course object - `course_key` id of course (a CourseKey) - `students` string of student emails separated by commas or returns (a `str`) - `overload` un-enrolls all existing students (a `boolean`) - `auto_enroll` is user input preference (a `boolean`) - `email_students` is user input preference (a `boolean`) - """ - - new_students, new_students_lc = get_and_clean_student_list(students) - status = dict([x, 'unprocessed'] for x in new_students) - - if overload: # delete all but staff - todelete = CourseEnrollment.objects.filter(course_id=course_key) - for enrollee in todelete: - if not has_access(enrollee.user, 'staff', course) and enrollee.user.email.lower() not in new_students_lc: - status[enrollee.user.email] = 'deleted' - enrollee.deactivate() - else: - status[enrollee.user.email] = 'is staff' - ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) - for cea in ceaset: - status[cea.email] = 'removed from pending enrollment list' - ceaset.delete() - - if email_students: - protocol = 'https' if secure else 'http' - stripped_site_name = microsite.get_value( - 'SITE_NAME', - settings.SITE_NAME - ) - # TODO: Use request.build_absolute_uri rather than '{proto}://{site}{path}'.format - # and check with the Services team that this works well with microsites - registration_url = '{proto}://{site}{path}'.format( - proto=protocol, - site=stripped_site_name, - path=reverse('register_user') - ) - course_url = '{proto}://{site}{path}'.format( - proto=protocol, - site=stripped_site_name, - path=reverse('course_root', kwargs={'course_id': course_key.to_deprecated_string()}) - ) - # We can't get the url to the course's About page if the marketing site is enabled. - course_about_url = None - if not settings.FEATURES.get('ENABLE_MKTG_SITE', False): - course_about_url = u'{proto}://{site}{path}'.format( - proto=protocol, - site=stripped_site_name, - path=reverse('about_course', kwargs={'course_id': course_key.to_deprecated_string()}) - ) - - # Composition of email - email_data = { - 'site_name': stripped_site_name, - 'registration_url': registration_url, - 'course': course, - 'auto_enroll': auto_enroll, - 'course_url': course_url, - 'course_about_url': course_about_url, - 'is_shib_course': is_shib_course - } - - for student in new_students: - try: - user = User.objects.get(email=student) - except User.DoesNotExist: - - # Student not signed up yet, put in pending enrollment allowed table - cea = CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_key) - - # If enrollmentallowed already exists, update auto_enroll flag to however it was set in UI - # Will be 0 or 1 records as there is a unique key on email + course_id - if cea: - cea[0].auto_enroll = auto_enroll - cea[0].save() - status[student] = 'user does not exist, enrollment already allowed, pending with auto enrollment ' \ - + ('on' if auto_enroll else 'off') - continue - - # EnrollmentAllowed doesn't exist so create it - cea = CourseEnrollmentAllowed(email=student, course_id=course_key, auto_enroll=auto_enroll) - cea.save() - - status[student] = 'user does not exist, enrollment allowed, pending with auto enrollment ' \ - + ('on' if auto_enroll else 'off') - - if email_students: - # User is allowed to enroll but has not signed up yet - email_data['email_address'] = student - email_data['message'] = 'allowed_enroll' - send_mail_ret = send_mail_to_student(student, email_data) - status[student] += (', email sent' if send_mail_ret else '') - continue - - # Student has already registered - if CourseEnrollment.is_enrolled(user, course_key): - status[student] = 'already enrolled' - continue - - try: - # Not enrolled yet - CourseEnrollment.enroll(user, course_key) - status[student] = 'added' - - if email_students: - # User enrolled for first time, populate dict with user specific info - email_data['email_address'] = student - email_data['full_name'] = user.profile.name - email_data['message'] = 'enrolled_enroll' - send_mail_ret = send_mail_to_student(student, email_data) - status[student] += (', email sent' if send_mail_ret else '') - - except Exception: # pylint: disable=broad-except - status[student] = 'rejected' - - datatable = {'header': ['StudentEmail', 'action']} - datatable['data'] = [[x, status[x]] for x in sorted(status)] - datatable['title'] = _('Enrollment of students') - - def sf(stat): # pylint: disable=invalid-name - return [x for x in status if status[x] == stat] - - data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'), - deleted=sf('deleted'), datatable=datatable) - - return data - - -#Unenrollment -def _do_unenroll_students(course_key, students, email_students=False): - """ - Do the actual work of un-enrolling multiple students, presented as a string - of emails separated by commas or returns - `course_key` is id of course (a `str`) - `students` is string of student emails separated by commas or returns (a `str`) - `email_students` is user input preference (a `boolean`) - """ - - old_students, __ = get_and_clean_student_list(students) - status = dict([x, 'unprocessed'] for x in old_students) - - stripped_site_name = microsite.get_value( - 'SITE_NAME', - settings.SITE_NAME - ) - if email_students: - course = modulestore().get_course(course_key) - # Composition of email - data = { - 'site_name': stripped_site_name, - 'course': course - } - - for student in old_students: - - isok = False - cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student) - # Will be 0 or 1 records as there is a unique key on email + course_id - if cea: - cea[0].delete() - status[student] = "un-enrolled" - isok = True - - try: - user = User.objects.get(email=student) - except User.DoesNotExist: - - if isok and email_students: - # User was allowed to join but had not signed up yet - data['email_address'] = student - data['message'] = 'allowed_unenroll' - send_mail_ret = send_mail_to_student(student, data) - status[student] += (', email sent' if send_mail_ret else '') - - continue - - # Will be 0 or 1 records as there is a unique key on user + course_id - if CourseEnrollment.is_enrolled(user, course_key): - try: - CourseEnrollment.unenroll(user, course_key) - status[student] = "un-enrolled" - if email_students: - # User was enrolled - data['email_address'] = student - data['full_name'] = user.profile.name - data['message'] = 'enrolled_unenroll' - send_mail_ret = send_mail_to_student(student, data) - status[student] += (', email sent' if send_mail_ret else '') - - except Exception: # pylint: disable=broad-except - if not isok: - status[student] = "Error! Failed to un-enroll" - - datatable = {'header': ['StudentEmail', 'action']} - datatable['data'] = [[x, status[x]] for x in sorted(status)] - datatable['title'] = _('Un-enrollment of students') - - return dict(datatable=datatable) - - -def send_mail_to_student(student, param_dict): - """ - Construct the email using templates and then send it. - `student` is the student's email address (a `str`), - - `param_dict` is a `dict` with keys [ - `site_name`: name given to edX instance (a `str`) - `registration_url`: url for registration (a `str`) - `course_key`: id of course (a CourseKey) - `auto_enroll`: user input option (a `str`) - `course_url`: url of course (a `str`) - `email_address`: email of student (a `str`) - `full_name`: student full name (a `str`) - `message`: type of email to send and template to use (a `str`) - `is_shib_course`: (a `boolean`) - ] - Returns a boolean indicating whether the email was sent successfully. - """ - - # add some helpers and microconfig subsitutions - if 'course' in param_dict: - param_dict['course_name'] = param_dict['course'].display_name_with_default - param_dict['site_name'] = microsite.get_value( - 'SITE_NAME', - param_dict.get('site_name', '') - ) - - subject = None - message = None - - message_type = param_dict['message'] - - email_template_dict = { - 'allowed_enroll': ('emails/enroll_email_allowedsubject.txt', 'emails/enroll_email_allowedmessage.txt'), - 'enrolled_enroll': ('emails/enroll_email_enrolledsubject.txt', 'emails/enroll_email_enrolledmessage.txt'), - 'allowed_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_allowedmessage.txt'), - 'enrolled_unenroll': ('emails/unenroll_email_subject.txt', 'emails/unenroll_email_enrolledmessage.txt'), - } - - subject_template, message_template = email_template_dict.get(message_type, (None, None)) - if subject_template is not None and message_template is not None: - subject = render_to_string(subject_template, param_dict) - message = render_to_string(message_template, param_dict) - - if subject and message: - # Remove leading and trailing whitespace from body - message = message.strip() - - # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) - from_address = microsite.get_value( - 'email_from_address', - settings.DEFAULT_FROM_EMAIL - ) - - send_mail(subject, message, from_address, [student], fail_silently=False) - - return True - else: - return False - - -def get_and_clean_student_list(students): - """ - Separate out individual student email from the comma, or space separated string. - `students` is string of student emails separated by commas or returns (a `str`) - Returns: - students: list of cleaned student emails - students_lc: list of lower case cleaned student emails - """ - - students = split_by_comma_and_whitespace(students) - students = [unicode(s.strip()) for s in students] - students = [s for s in students if s != ''] - students_lc = [x.lower() for x in students] - - return students, students_lc - -#----------------------------------------------------------------------------- -# answer distribution - - -def get_answers_distribution(request, course_key): - """ - Get the distribution of answers for all graded problems in the course. - - Return a dict with two keys: - 'header': a header row - 'data': a list of rows - """ - course = get_course_with_access(request.user, 'staff', course_key) - - course_answer_distributions = grades.answer_distributions(course.id) - - dist = {} - dist['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count'] - - dist['data'] = [ - [url_name, display_name, answer_id, a, answers[a]] - for (url_name, display_name, answer_id), answers in sorted(course_answer_distributions.items()) - for a in answers - ] - return dist - - -#----------------------------------------------------------------------------- - - -def compute_course_stats(course): - """ - Compute course statistics, including number of problems, videos, html. - - course is a CourseDescriptor from the xmodule system. - """ - - # walk the course by using get_children() until we come to the leaves; count the - # number of different leaf types - - counts = defaultdict(int) - - def walk(module): - children = module.get_children() - category = module.__class__.__name__ # HtmlDescriptor, CapaDescriptor, ... - counts[category] += 1 - for child in children: - walk(child) - - walk(course) - stats = dict(counts) # number of each kind of module - return stats - - -def dump_grading_context(course): - """ - Dump information about course grading context (eg which problems are graded in what assignments) - Very useful for debugging grading_policy.json and policy.json - """ - msg = "-----------------------------------------------------------------------------\n" - msg += "Course grader:\n" - - msg += '%s\n' % course.grader.__class__ - graders = {} - if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader): - msg += '\n' - msg += "Graded sections:\n" - for subgrader, category, weight in course.grader.sections: - msg += " subgrader=%s, type=%s, category=%s, weight=%s\n" % (subgrader.__class__, subgrader.type, category, weight) - subgrader.index = 1 - graders[subgrader.type] = subgrader - msg += "-----------------------------------------------------------------------------\n" - msg += "Listing grading context for course %s\n" % course.id - - gcontext = course.grading_context - msg += "graded sections:\n" - - msg += '%s\n' % gcontext['graded_sections'].keys() - for (gsections, gsvals) in gcontext['graded_sections'].items(): - msg += "--> Section %s:\n" % (gsections) - for sec in gsvals: - sdesc = sec['section_descriptor'] - grade_format = getattr(sdesc, 'grade_format', None) - aname = '' - if grade_format in graders: - gfmt = graders[grade_format] - aname = '%s %02d' % (gfmt.short_label, gfmt.index) - gfmt.index += 1 - elif sdesc.display_name in graders: - gfmt = graders[sdesc.display_name] - aname = '%s' % gfmt.short_label - notes = '' - if getattr(sdesc, 'score_by_attempt', False): - notes = ', score by attempt!' - msg += " %s (grade_format=%s, Assignment=%s%s)\n" % (sdesc.display_name, grade_format, aname, notes) - msg += "all descriptors:\n" - msg += "length=%d\n" % len(gcontext['all_descriptors']) - msg = '
%s
' % msg.replace('<', '<') - return msg - - -def get_background_task_table(course_key, problem_url=None, student=None, task_type=None): - """ - Construct the "datatable" structure to represent background task history. - - Filters the background task history to the specified course and problem. - If a student is provided, filters to only those tasks for which that student - was specified. - - Returns a tuple of (msg, datatable), where the msg is a possible error message, - and the datatable is the datatable to be used for display. - """ - history_entries = get_instructor_task_history(course_key, problem_url, student, task_type) - datatable = {} - msg = "" - # first check to see if there is any history at all - # (note that we don't have to check that the arguments are valid; it - # just won't find any entries.) - if (history_entries.count()) == 0: - if problem_url is None: - msg += 'Failed to find any background tasks for course "{course}".'.format( - course=course_key.to_deprecated_string() - ) - elif student is not None: - template = '' + _('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '' - msg += template.format(course=course_key.to_deprecated_string(), problem=problem_url, student=student.username) - else: - msg += '' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format( - course=course_key.to_deprecated_string(), problem=problem_url - ) + '' - else: - datatable['header'] = ["Task Type", - "Task Id", - "Requester", - "Submitted", - "Duration (sec)", - "Task State", - "Task Status", - "Task Output"] - - datatable['data'] = [] - for instructor_task in history_entries: - # get duration info, if known: - duration_sec = 'unknown' - if hasattr(instructor_task, 'task_output') and instructor_task.task_output is not None: - task_output = json.loads(instructor_task.task_output) - if 'duration_ms' in task_output: - duration_sec = int(task_output['duration_ms'] / 1000.0) - # get progress status message: - success, task_message = get_task_completion_info(instructor_task) - status = "Complete" if success else "Incomplete" - # generate row for this task: - row = [ - str(instructor_task.task_type), - str(instructor_task.task_id), - str(instructor_task.requester), - instructor_task.created.isoformat(' '), - duration_sec, - str(instructor_task.task_state), - status, - task_message - ] - datatable['data'].append(row) - - if problem_url is None: - datatable['title'] = "{course_id}".format(course_id=course_key.to_deprecated_string()) - elif student is not None: - datatable['title'] = "{course_id} > {location} > {student}".format( - course_id=course_key.to_deprecated_string(), - location=problem_url, - student=student.username - ) - else: - datatable['title'] = "{course_id} > {location}".format( - course_id=course_key.to_deprecated_string(), location=problem_url - ) - - return msg, datatable - - -def uses_shib(course): - """ - Used to return whether course has Shibboleth as the enrollment domain - - Returns a boolean indicating if Shibboleth authentication is set for this course. - """ - return course.enrollment_domain and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) diff --git a/lms/envs/common.py b/lms/envs/common.py index ef0528df20..0d4cce8722 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -205,9 +205,6 @@ FEATURES = { # Enable Custom Courses for EdX 'CUSTOM_COURSES_EDX': False, - # Enable legacy instructor dashboard - 'ENABLE_INSTRUCTOR_LEGACY_DASHBOARD': False, - # Is this an edX-owned domain? (used for edX specific messaging and images) 'IS_EDX_DOMAIN': False, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 5c48e6ca0b..f84fac987d 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -28,7 +28,6 @@ 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_LEGACY_DASHBOARD'] = False FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index 8ec7054124..582e825764 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -62,8 +62,6 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'] = True - FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['ENABLE_VERIFIED_CERTIFICATES'] = True diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss index 2883ab38e6..d2ebc453b1 100644 --- a/lms/static/sass/_build-course.scss +++ b/lms/static/sass/_build-course.scss @@ -49,7 +49,6 @@ @import "views/teams"; // course - instructor-only views -@import "course/instructor/instructor"; @import "course/instructor/instructor_2"; @import "course/instructor/email"; @import "xmodule/descriptors/css/module-styles.scss"; diff --git a/lms/static/sass/course/instructor/_instructor.scss b/lms/static/sass/course/instructor/_instructor.scss deleted file mode 100644 index f153459559..0000000000 --- a/lms/static/sass/course/instructor/_instructor.scss +++ /dev/null @@ -1,189 +0,0 @@ -.instructor-dashboard-wrapper { - display: table; - position: relative; - - .beta-button-wrapper { - position: absolute; - top: 2em; - right: 2em; - } - - .studio-edit-link{ - position: absolute; - top: 3.5em; - right: 2em; - } - - section.instructor-dashboard-content { - @extend .content; - padding: 40px; - width: 100%; - - h1 { - @extend .top-header; - } - } - - // form fields - .list-fields { - @extend %ui-no-list; - - .field { - margin-bottom: $baseline; - - &:last-child { - margin-bottom: 0; - } - - .tip { - display: block; - margin-top: ($baseline/4); - color: tint(rgb(127,127,127),50%); - @include font-size(12); - } - - } - - } - - // ==================== - - // system feedback - messages - .msg { - border-radius: 1px; - padding: 10px 15px; - margin-bottom: $baseline; - - .copy { - font-weight: 600; - } - } - - // TYPE: warning - .msg-warning { - border-top: 2px solid $warning-color; - background: tint($warning-color,95%); - - .copy { - color: $warning-color; - } - } - - // TYPE: confirm - .msg-confirm { - border-top: 2px solid $confirm-color; - background: tint($confirm-color,95%); - - .copy { - color: $confirm-color; - } - } - - // TYPE: confirm - .msg-error { - border-top: 2px solid $error-color; - background: tint($error-color,95%); - - .copy { - color: $error-color; - } - } - - // ==================== - - // inline copy - .copy-confirm { - color: $confirm-color; - } - - .copy-warning { - color: $warning-color; - } - - .copy-error { - color: $error-color; - } - - .list-advice { - list-style: none; - padding: 0; - margin: 20px 0; - - .item { - font-weight: 600; - margin-bottom: ($baseline/2); - - &:last-child { - margin-bottom: 0; - } - } - } - - //Metrics tab - - .metrics-container { - position: relative; - width: 100%; - float: left; - clear: both; - margin-top: 25px; - } - .metrics-left { - position: relative; - width: 30%; - height: 640px; - float: left; - margin-right: 2.5%; - } - .metrics-right { - position: relative; - width: 65%; - height: 295px; - float: left; - margin-left: 2.5%; - margin-bottom: 25px; - } - .metrics-tooltip { - width: 250px; - background-color: lightgray; - padding: 3px; - } - .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; - } - - .wrapper-msg { - margin-bottom: ($baseline*1.5); - - .msg { - margin-bottom: 0; - } - - .note { - margin: 0; - } - } - -} - -.rtl .instructor-dashboard-wrapper .beta-button-wrapper, -.rtl .instructor-dashboard-wrapper .studio-edit-link { - left: 2em; - right: auto; -} diff --git a/lms/templates/courseware/legacy_instructor_dashboard.html b/lms/templates/courseware/legacy_instructor_dashboard.html deleted file mode 100644 index 2ec867f363..0000000000 --- a/lms/templates/courseware/legacy_instructor_dashboard.html +++ /dev/null @@ -1,496 +0,0 @@ -## 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 ## - -<%inherit file="../main.html" /> -<%namespace name='static' file='/static_content.html'/> -<%! -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse -%> - -<%block name="pagetitle">${_("Legacy Instructor Dashboard")} -<%block name="nav_skip">#instructor-dashboard-content - -<%block name="headextra"> -<%static:css group='style-course-vendor'/> -<%static:css group='style-vendor-tinymce-content'/> -<%static:css group='style-vendor-tinymce-skin'/> -<%static:css group='style-course'/> - - - - - - - - - - - - <%static:js group='module-descriptor-js'/> -%if instructor_tasks is not None: - -%endif - - -<%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> - - - - - -
-
- -
- - -

${_("Legacy 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 - - - -
- - - -##----------------------------------------------------------------------------- -%if modeflag.get('Grades'): - - %if offline_grade_log: -

- Pre-computed grades ${offline_grade_log} available: Use? - - -

- %endif - - -
-

${_("Grade Downloads")}

- % if disable_buttons: - -
- -
-

- ${_("Note: some of these buttons are known to time out for larger " - "courses. We have disabled those features for courses " - "with more than {max_enrollment} students.").format( - max_enrollment=settings.FEATURES['MAX_ENROLLMENT_INSTR_BUTTONS'] - )} -

-
-
- % endif - -

- -

- -

- - -

- -

- %if not settings.FEATURES.get('ENABLE_ASYNC_ANSWER_DISTRIBUTION'): - - %endif -

- ${_("To download student grades and view the grading configuration for your course, visit the Data Download section of the Instructor Dashboard.")} -

-

- ${_("To view the Gradebook (only available for courses with a small number of enrolled students), visit the Student Admin section of the Instructor Dashboard.")} -

-

-
- - %if settings.FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: - - <% - rg = course.remote_gradebook - %> - -

${_("Export grades to remote gradebook")}

-

${_("The assignments defined for this course should match the ones stored in the gradebook, for this to work properly!")}

- -
    -
  • ${_("Gradebook name:")} ${rg.get('name','None defined!')} -
    -
    - - -
    -
    -
  • -
  • -
    -
    -
  • -
  • ${_("Assignment name:")} -
    -
    - - - -
  • -
-
- - %endif - %if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): -

${_("Course-specific grade adjustment")}

- -

${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}

- - %endif - -

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

- -

${_("To perform these actions, visit the Student Admin section of the Instructor Dashboard.")}

- - %endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Admin'): - - %if instructor_access or admin_access: -

${_("To add or remove course staff or instructors, visit the Membership section of the Instructor Dashboard.")}

- %endif - - %if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access: -

- - - %endif -%endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Forum Admin'): -

${_("To manage forum roles, visit the Membership section of the Instructor Dashboard.")}

-%endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Enrollment'): - -
-

${_("Enrollment Data")}

- % if disable_buttons: - -
-
-

- ${_("Note: some of these buttons are known to time out for larger " - "courses. We have disabled those features for courses " - "with more than {max_enrollment} students.").format( - max_enrollment=settings.FEATURES['MAX_ENROLLMENT_INSTR_BUTTONS'] - )} -

-
-
- % endif - -

- ${_("To download a CSV file containing profile information for students who are enrolled in this course, visit the Data Download section of the Instructor Dashboard.")} -

- -

- ${_("To download a list of students who may enroll in this course but have not yet signed up for it, visit the Data Download section of the Instructor Dashboard.")} -

- -
- - %if settings.FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: - - <% - rg = course.remote_gradebook - %> - -

${_("Pull enrollment from remote gradebook")}

-
    -
  • ${_("Gradebook name:")} ${rg.get('name','None defined!')} -
  • ${_("Section:")}
  • -
- - - - -
- - %endif -%endif - -##----------------------------------------------------------------------------- - -%if modeflag.get('Data'): -
-

- ${_("To download a CSV listing student responses to a given problem, visit the Data Download section of the Instructor Dashboard.")} -

- -

- ${_("To download student profile data and anonymized IDs, visit the Data Download section of the Instructor Dashboard.")} -

-
-%endif - -##----------------------------------------------------------------------------- - -%if modeflag.get('Manage Groups'): - %if instructor_access: - %if course_is_cohorted: -

${_("To manage beta tester roles and cohorts, visit the Membership section of the Instructor Dashboard.")}

- %else: -

${_("To manage beta tester roles, visit the Membership section of the Instructor Dashboard.")}

- %endif - %endif -%endif - -##----------------------------------------------------------------------------- - -%if modeflag.get('Email'): -

${_("To send email, visit the Email section of the Instructor Dashboard.")}

-%endif - -
-##----------------------------------------------------------------------------- - -%if msg: -

${msg}

-%endif - -##----------------------------------------------------------------------------- - -%if datatable: - -
-
-

-


-

${datatable['title'] | h}

- - - %for hname in datatable['header']: - - %endfor - - %for row in datatable['data']: - - %for value in row: - - %endfor - - %endfor -
${hname | h}
${value | h}
-

-%endif - -## Output tasks in progress - -%if instructor_tasks is not None and len(instructor_tasks) > 0: -
-

${_("Pending Instructor Tasks")}

-
- - - - - - - - - - - - %for tasknum, instructor_task in enumerate(instructor_tasks): - - - - - - - - - - - %endfor -
${_("Task Type")}${_("Task inputs")}${_("Task Id")}${_("Requester")}${_("Submitted")}${_("Task State")}${_("Duration (sec)")}${_("Task Progress")}
${instructor_task.task_type}${instructor_task.task_input}${instructor_task.task_id}${instructor_task.requester}${instructor_task.created}${instructor_task.task_state}${_("unknown")}${_("unknown")}
-
-
- -%endif - -##----------------------------------------------------------------------------- - -%if modeflag.get('Admin') and course_stats: -
-
-

-


-

${course_stats['title'] | h}

- - - %for hname in course_stats['header']: - - %endfor - - %for row in course_stats['data']: - - %for value in row: - - %endfor - - %endfor -
${hname | h}
${value | h}
-

-%else: -
-
-

${_("Course Statistics At A Glance")}

-

- ${_("View course statistics in the Admin section of this legacy instructor dashboard.")} -

-%endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Admin'): - % if course_errors is not UNDEFINED: -

${_("Course errors")}

-
- %if not course_errors: - None - %else: -
    - % for (summary, err) in course_errors: -
  • ${summary | h} - % if err: -
    • ${err | h}
    - % else: -

     

    - % endif -
  • - % endfor -
- %endif -
- % endif -%endif - -
-
-
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 e657dcb847..d04041a8dd 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -91,9 +91,6 @@ from django.core.urlresolvers import reverse %if studio_url: ${_("View Course in Studio")} %endif - %if settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): - ${_("Revert to Legacy Dashboard")} - %endif

${_("Instructor Dashboard")}

diff --git a/lms/urls.py b/lms/urls.py index 1e50b236a7..1d7d984fdd 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -547,7 +547,6 @@ urlpatterns += ( ), include(COURSE_URLS) ), - # see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls # Cohorts management url( @@ -753,13 +752,6 @@ if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'): ), ) - -if settings.FEATURES.get('ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'): - urlpatterns += ( - url(r'^courses/{}/legacy_instructor_dash$'.format(settings.COURSE_ID_PATTERN), - 'instructor.views.legacy.instructor_dashboard', name="instructor_dashboard_legacy"), - ) - if settings.FEATURES.get('CLASS_DASHBOARD'): urlpatterns += ( url(r'^class_dashboard/', include('class_dashboard.urls')),