From a9fc7aca69306b87c0325df3d5846ebb58eb60af Mon Sep 17 00:00:00 2001
From: Sarina Canelake ".format(data_dir)
- msg += "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}:{0}
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 += '{1}".format(cmsg, escape(cerr))
- 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 += "
{msg}'.format(msg=retdict['msg'].replace('\n', '%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> -<%block name="nav_skip">#instructor-dashboard-content%block> - -<%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 -%block> - -<%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> - - - - - -
${_("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.")}
-${msg}
-%endif - -##----------------------------------------------------------------------------- - -%if datatable: - --
| ${hname | h} | - %endfor -
|---|
| ${value | h} | - %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")} | -
-
| ${hname | h} | - %endfor -
|---|
| ${value | h} | - %endfor -
- ${_("View course statistics in the Admin section of this legacy instructor dashboard.")} -
-%endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Admin'): - % if course_errors is not UNDEFINED: -${err | h}- % endif -